Auto-compact Threshold UI improvements (#5354)

This commit is contained in:
David Katz
2025-10-27 15:22:06 -04:00
committed by GitHub
parent c021e00893
commit 77fe4f329f
4 changed files with 94 additions and 88 deletions
+1 -1
View File
@@ -201,7 +201,7 @@ pub async fn check_if_compaction_needed(
let usage_ratio = current_tokens as f64 / context_limit as f64; let usage_ratio = current_tokens as f64 / context_limit as f64;
let needs_compaction = if threshold <= 0.0 || threshold >= 1.0 { let needs_compaction = if threshold <= 0.0 || threshold >= 1.0 {
usage_ratio > DEFAULT_COMPACTION_THRESHOLD false // Auto-compact is disabled.
} else { } else {
usage_ratio > threshold usage_ratio > threshold
}; };
+1 -30
View File
@@ -142,7 +142,6 @@ export default function ChatInput({
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT); const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
const [autoCompactThreshold, setAutoCompactThreshold] = useState<number>(0.8); // Default to 80%
// Draft functionality - get chat context and global draft context // Draft functionality - get chat context and global draft context
// We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub) // We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub)
@@ -501,22 +500,6 @@ export default function ChatInput({
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentModel, currentProvider]); }, [currentModel, currentProvider]);
// Load auto-compact threshold
const loadAutoCompactThreshold = useCallback(async () => {
try {
const threshold = await read('GOOSE_AUTO_COMPACT_THRESHOLD', false);
if (threshold !== undefined && threshold !== null) {
setAutoCompactThreshold(threshold as number);
}
} catch (err) {
console.error('Error fetching auto-compact threshold:', err);
}
}, [read]);
useEffect(() => {
loadAutoCompactThreshold();
}, [loadAutoCompactThreshold]);
// Handle tool count alerts and token usage // Handle tool count alerts and token usage
useEffect(() => { useEffect(() => {
clearAlerts(); clearAlerts();
@@ -542,10 +525,6 @@ export default function ChatInput({
handleSubmit(customEvent); handleSubmit(customEvent);
}, },
compactIcon: <ScrollText size={12} />, compactIcon: <ScrollText size={12} />,
autoCompactThreshold: autoCompactThreshold,
onThresholdChange: (newThreshold: number) => {
setAutoCompactThreshold(newThreshold);
},
}); });
} }
@@ -563,15 +542,7 @@ export default function ChatInput({
} }
// We intentionally omit setView as it shouldn't trigger a re-render of alerts // We intentionally omit setView as it shouldn't trigger a re-render of alerts
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [ }, [numTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]);
numTokens,
toolCount,
tokenLimit,
isTokenLimitLoaded,
addAlert,
clearAlerts,
autoCompactThreshold,
]);
// Cleanup effect for component unmount - prevent memory leaks // Cleanup effect for component unmount - prevent memory leaks
useEffect(() => { useEffect(() => {
+85 -57
View File
@@ -1,9 +1,10 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io'; import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io';
import { FaPencilAlt, FaSave } from 'react-icons/fa'; import { FaPencilAlt, FaSave } from 'react-icons/fa';
import { cn } from '../../utils'; import { cn } from '../../utils';
import { Alert, AlertType } from './types'; import { Alert, AlertType } from './types';
import { upsertConfig } from '../../api'; import { upsertConfig } from '../../api';
import { useConfig } from '../ConfigContext';
const alertIcons: Record<AlertType, React.ReactNode> = { const alertIcons: Record<AlertType, React.ReactNode> = {
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />, [AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
@@ -24,17 +25,36 @@ const alertStyles: Record<AlertType, string> = {
}; };
export const AlertBox = ({ alert, className }: AlertBoxProps) => { export const AlertBox = ({ alert, className }: AlertBoxProps) => {
const { read } = useConfig();
const [isEditingThreshold, setIsEditingThreshold] = useState(false); const [isEditingThreshold, setIsEditingThreshold] = useState(false);
const [loadedThreshold, setLoadedThreshold] = useState<number | null>(null);
const [thresholdValue, setThresholdValue] = useState( const [thresholdValue, setThresholdValue] = useState(
alert.autoCompactThreshold ? Math.round(alert.autoCompactThreshold * 100) : 80 alert.autoCompactThreshold ? Math.max(1, Math.round(alert.autoCompactThreshold * 100)) : 80
); );
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
useEffect(() => {
const loadThreshold = async () => {
try {
const threshold = await read('GOOSE_AUTO_COMPACT_THRESHOLD', false);
if (threshold !== undefined && threshold !== null) {
setLoadedThreshold(threshold as number);
setThresholdValue(Math.max(1, Math.round((threshold as number) * 100)));
}
} catch (err) {
console.error('Error fetching auto-compact threshold:', err);
}
};
loadThreshold();
}, [read]);
const currentThreshold = loadedThreshold !== null ? loadedThreshold : alert.autoCompactThreshold;
const handleSaveThreshold = async () => { const handleSaveThreshold = async () => {
if (isSaving) return; // Prevent double-clicks if (isSaving) return; // Prevent double-clicks
// Validate threshold value - allow 0 and 100 as special values to disable let validThreshold = Math.max(1, Math.min(100, thresholdValue));
const validThreshold = Math.max(0, Math.min(100, thresholdValue));
if (validThreshold !== thresholdValue) { if (validThreshold !== thresholdValue) {
setThresholdValue(validThreshold); setThresholdValue(validThreshold);
} }
@@ -52,6 +72,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
}); });
setIsEditingThreshold(false); setIsEditingThreshold(false);
setLoadedThreshold(newThreshold);
// Notify parent component of the threshold change // Notify parent component of the threshold change
if (alert.onThresholdChange) { if (alert.onThresholdChange) {
@@ -82,14 +103,14 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
<span className="text-[11px]">{alert.message}</span> <span className="text-[11px]">{alert.message}</span>
{/* Auto-compact threshold indicator with edit */} {/* Auto-compact threshold indicator with edit */}
{alert.autoCompactThreshold !== undefined && ( {currentThreshold !== undefined && (
<div className="flex items-center justify-center gap-1 min-h-[20px]"> <div className="flex items-center justify-center gap-1 min-h-[20px]">
{isEditingThreshold ? ( {isEditingThreshold ? (
<> <>
<span className="text-[10px] opacity-70">Auto summarize at</span> <span className="text-[10px] opacity-70">Auto compact at</span>
<input <input
type="number" type="number"
min="0" min="1"
max="100" max="100"
step="1" step="1"
value={thresholdValue} value={thresholdValue}
@@ -97,17 +118,17 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
const val = parseInt(e.target.value, 10); const val = parseInt(e.target.value, 10);
// Allow empty input for easier editing // Allow empty input for easier editing
if (e.target.value === '') { if (e.target.value === '') {
setThresholdValue(0); setThresholdValue(1);
} else if (!isNaN(val)) { } else if (!isNaN(val)) {
// Clamp value between 0 and 100 // Clamp value between 1 and 100
setThresholdValue(Math.max(0, Math.min(100, val))); setThresholdValue(Math.max(1, Math.min(100, val)));
} }
}} }}
onBlur={(e) => { onBlur={(e) => {
// On blur, ensure we have a valid value // On blur, ensure we have a valid value
const val = parseInt(e.target.value, 10); const val = parseInt(e.target.value, 10);
if (isNaN(val) || val < 0) { if (isNaN(val) || val < 1) {
setThresholdValue(0); setThresholdValue(1);
} else if (val > 100) { } else if (val > 100) {
setThresholdValue(100); setThresholdValue(100);
} }
@@ -117,11 +138,10 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
handleSaveThreshold(); handleSaveThreshold();
} else if (e.key === 'Escape') { } else if (e.key === 'Escape') {
setIsEditingThreshold(false); setIsEditingThreshold(false);
setThresholdValue( const resetValue = currentThreshold
alert.autoCompactThreshold ? Math.round(currentThreshold * 100)
? Math.round(alert.autoCompactThreshold * 100) : 80;
: 80 setThresholdValue(Math.max(1, resetValue));
);
} }
}} }}
onFocus={(e) => { onFocus={(e) => {
@@ -154,9 +174,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
) : ( ) : (
<> <>
<span className="text-[10px] opacity-70"> <span className="text-[10px] opacity-70">
{alert.autoCompactThreshold === 0 || alert.autoCompactThreshold === 1 Auto compact at {Math.round(currentThreshold * 100)}%
? 'Auto summarize disabled'
: `Auto summarize at ${Math.round(alert.autoCompactThreshold * 100)}%`}
</span> </span>
<button <button
type="button" type="button"
@@ -176,46 +194,56 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
)} )}
<div className="flex justify-between w-full relative"> <div className="flex justify-between w-full relative">
{[...Array(30)].map((_, i) => { {(() => {
const progress = alert.progress!.current / alert.progress!.total; let closestDotIndex = -1;
const progressPercentage = Math.round(progress * 100); if (currentThreshold !== undefined && currentThreshold > 0 && currentThreshold <= 1) {
const dotPosition = i / 29; // 0 to 1 range for 30 dots let minDistance = Infinity;
const isActive = dotPosition <= progress; for (let j = 0; j < 30; j++) {
const isThresholdDot = const dotPos = j / 29;
alert.autoCompactThreshold !== undefined && const distance = Math.abs(dotPos - currentThreshold);
alert.autoCompactThreshold > 0 && if (distance < minDistance) {
alert.autoCompactThreshold < 1 && minDistance = distance;
Math.abs(dotPosition - alert.autoCompactThreshold) < 0.017; // ~1/30 tolerance closestDotIndex = j;
}
// Determine the color based on progress percentage
const getProgressColor = () => {
if (progressPercentage <= 50) {
return 'bg-green-500'; // Green for 0-50%
} else if (progressPercentage <= 75) {
return 'bg-yellow-500'; // Yellow for 51-75%
} else if (progressPercentage <= 90) {
return 'bg-orange-500'; // Orange for 76-90%
} else {
return 'bg-red-500'; // Red for 91-100%
} }
}; }
const progressColor = getProgressColor(); return [...Array(30)].map((_, i) => {
const inactiveColor = 'bg-gray-300 dark:bg-gray-600'; const progress = alert.progress!.current / alert.progress!.total;
const progressPercentage = Math.round(progress * 100);
const dotPosition = i / 29; // 0 to 1 range for 30 dots
const isActive = dotPosition <= progress;
const isThresholdDot = i === closestDotIndex;
return ( const getProgressColor = () => {
<div if (progressPercentage <= 50) {
key={i} return 'bg-green-500';
className={cn( } else if (progressPercentage <= 75) {
'rounded-full transition-all relative', return 'bg-yellow-500';
isThresholdDot } else if (progressPercentage <= 90) {
? 'h-[6px] w-[6px] -mt-[2px]' // Make threshold dot twice as large return 'bg-orange-500';
: 'h-[2px] w-[2px]', } else {
isActive ? progressColor : inactiveColor return 'bg-red-500';
)} }
/> };
);
})} const progressColor = getProgressColor();
const inactiveColor = 'bg-gray-300 dark:bg-gray-600';
return (
<div
key={i}
className={cn(
'rounded-full transition-all relative',
isThresholdDot
? 'h-[6px] w-[6px] -mt-[2px]' // Make threshold dot twice as large
: 'h-[2px] w-[2px]',
isActive ? progressColor : inactiveColor
)}
/>
);
});
})()}
</div> </div>
<div className="flex justify-between items-baseline text-[11px]"> <div className="flex justify-between items-baseline text-[11px]">
<div className="flex gap-1 items-baseline"> <div className="flex gap-1 items-baseline">
@@ -4,6 +4,13 @@ import userEvent from '@testing-library/user-event';
import { AlertBox } from '../AlertBox'; import { AlertBox } from '../AlertBox';
import { Alert, AlertType } from '../types'; import { Alert, AlertType } from '../types';
// Mock the ConfigContext
vi.mock('../../ConfigContext', () => ({
useConfig: () => ({
read: vi.fn().mockResolvedValue(0.8),
}),
}));
describe('AlertBox', () => { describe('AlertBox', () => {
const mockOnCompact = vi.fn(); const mockOnCompact = vi.fn();