Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/goose/src/context_mgmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ pub async fn check_if_compaction_needed(
let usage_ratio = current_tokens as f64 / context_limit as f64;

let needs_compaction = if threshold <= 0.0 || threshold >= 1.0 {
usage_ratio > DEFAULT_COMPACTION_THRESHOLD
false // Auto-compact is disabled.
} else {
usage_ratio > threshold
};
Expand Down
31 changes: 1 addition & 30 deletions ui/desktop/src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ export default function ChatInput({
const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider();
const [tokenLimit, setTokenLimit] = useState<number>(TOKEN_LIMIT_DEFAULT);
const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false);
const [autoCompactThreshold, setAutoCompactThreshold] = useState<number>(0.8); // Default to 80%

// 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)
Expand Down Expand Up @@ -500,22 +499,6 @@ export default function ChatInput({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [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
useEffect(() => {
clearAlerts();
Expand All @@ -541,10 +524,6 @@ export default function ChatInput({
handleSubmit(customEvent);
},
compactIcon: <ScrollText size={12} />,
autoCompactThreshold: autoCompactThreshold,
onThresholdChange: (newThreshold: number) => {
setAutoCompactThreshold(newThreshold);
},
});
}

Expand All @@ -562,15 +541,7 @@ export default function ChatInput({
}
// We intentionally omit setView as it shouldn't trigger a re-render of alerts
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
numTokens,
toolCount,
tokenLimit,
isTokenLimitLoaded,
addAlert,
clearAlerts,
autoCompactThreshold,
]);
}, [numTokens, toolCount, tokenLimit, isTokenLimitLoaded, addAlert, clearAlerts]);

// Cleanup effect for component unmount - prevent memory leaks
useEffect(() => {
Expand Down
142 changes: 85 additions & 57 deletions ui/desktop/src/components/alerts/AlertBox.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import { IoIosCloseCircle, IoIosWarning, IoIosInformationCircle } from 'react-icons/io';
import { FaPencilAlt, FaSave } from 'react-icons/fa';
import { cn } from '../../utils';
import { Alert, AlertType } from './types';
import { upsertConfig } from '../../api';
import { useConfig } from '../ConfigContext';

const alertIcons: Record<AlertType, React.ReactNode> = {
[AlertType.Error]: <IoIosCloseCircle className="h-5 w-5" />,
Expand All @@ -24,17 +25,36 @@ const alertStyles: Record<AlertType, string> = {
};

export const AlertBox = ({ alert, className }: AlertBoxProps) => {
const { read } = useConfig();
const [isEditingThreshold, setIsEditingThreshold] = useState(false);
const [loadedThreshold, setLoadedThreshold] = useState<number | null>(null);
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);

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 () => {
if (isSaving) return; // Prevent double-clicks

// Validate threshold value - allow 0 and 100 as special values to disable
const validThreshold = Math.max(0, Math.min(100, thresholdValue));
let validThreshold = Math.max(1, Math.min(100, thresholdValue));
if (validThreshold !== thresholdValue) {
setThresholdValue(validThreshold);
}
Expand All @@ -52,6 +72,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
});

setIsEditingThreshold(false);
setLoadedThreshold(newThreshold);

// Notify parent component of the threshold change
if (alert.onThresholdChange) {
Expand Down Expand Up @@ -82,32 +103,32 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
<span className="text-[11px]">{alert.message}</span>

{/* Auto-compact threshold indicator with edit */}
{alert.autoCompactThreshold !== undefined && (
{currentThreshold !== undefined && (
<div className="flex items-center justify-center gap-1 min-h-[20px]">
{isEditingThreshold ? (
<>
<span className="text-[10px] opacity-70">Auto summarize at</span>
<span className="text-[10px] opacity-70">Auto compact at</span>
<input
type="number"
min="0"
min="1"
max="100"
step="1"
value={thresholdValue}
onChange={(e) => {
const val = parseInt(e.target.value, 10);
// Allow empty input for easier editing
if (e.target.value === '') {
setThresholdValue(0);
setThresholdValue(1);
} else if (!isNaN(val)) {
// Clamp value between 0 and 100
setThresholdValue(Math.max(0, Math.min(100, val)));
// Clamp value between 1 and 100
setThresholdValue(Math.max(1, Math.min(100, val)));
}
}}
onBlur={(e) => {
// On blur, ensure we have a valid value
const val = parseInt(e.target.value, 10);
if (isNaN(val) || val < 0) {
setThresholdValue(0);
if (isNaN(val) || val < 1) {
setThresholdValue(1);
} else if (val > 100) {
setThresholdValue(100);
}
Expand All @@ -117,11 +138,10 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
handleSaveThreshold();
} else if (e.key === 'Escape') {
setIsEditingThreshold(false);
setThresholdValue(
alert.autoCompactThreshold
? Math.round(alert.autoCompactThreshold * 100)
: 80
);
const resetValue = currentThreshold
? Math.round(currentThreshold * 100)
: 80;
setThresholdValue(Math.max(1, resetValue));
}
}}
onFocus={(e) => {
Expand Down Expand Up @@ -154,9 +174,7 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
) : (
<>
<span className="text-[10px] opacity-70">
{alert.autoCompactThreshold === 0 || alert.autoCompactThreshold === 1
? 'Auto summarize disabled'
: `Auto summarize at ${Math.round(alert.autoCompactThreshold * 100)}%`}
Auto compact at {Math.round(currentThreshold * 100)}%
</span>
<button
type="button"
Expand All @@ -176,46 +194,56 @@ export const AlertBox = ({ alert, className }: AlertBoxProps) => {
)}

<div className="flex justify-between w-full relative">
{[...Array(30)].map((_, i) => {
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 =
alert.autoCompactThreshold !== undefined &&
alert.autoCompactThreshold > 0 &&
alert.autoCompactThreshold < 1 &&
Math.abs(dotPosition - alert.autoCompactThreshold) < 0.017; // ~1/30 tolerance

// 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%
{(() => {
let closestDotIndex = -1;
if (currentThreshold !== undefined && currentThreshold > 0 && currentThreshold <= 1) {
let minDistance = Infinity;
for (let j = 0; j < 30; j++) {
const dotPos = j / 29;
const distance = Math.abs(dotPos - currentThreshold);
if (distance < minDistance) {
minDistance = distance;
closestDotIndex = j;
}
}
};
}

return [...Array(30)].map((_, i) => {
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;

const progressColor = getProgressColor();
const inactiveColor = 'bg-gray-300 dark:bg-gray-600';
const getProgressColor = () => {
if (progressPercentage <= 50) {
return 'bg-green-500';
} else if (progressPercentage <= 75) {
return 'bg-yellow-500';
} else if (progressPercentage <= 90) {
return 'bg-orange-500';
} else {
return 'bg-red-500';
}
};

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
)}
/>
);
})}
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 className="flex justify-between items-baseline text-[11px]">
<div className="flex gap-1 items-baseline">
Expand Down
7 changes: 7 additions & 0 deletions ui/desktop/src/components/alerts/__tests__/AlertBox.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import userEvent from '@testing-library/user-event';
import { AlertBox } from '../AlertBox';
import { Alert, AlertType } from '../types';

// Mock the ConfigContext
vi.mock('../../ConfigContext', () => ({
useConfig: () => ({
read: vi.fn().mockResolvedValue(0.8),
}),
}));

describe('AlertBox', () => {
const mockOnCompact = vi.fn();

Expand Down
Loading