Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2029900
feat: add auto-compaction with progressive warnings
ethanndickson Nov 21, 2025
3a272c7
fix merge conflict
ethanndickson Nov 21, 2025
7e67829
use model options for 1m context
ethanndickson Nov 21, 2025
683d9df
pass model and images when resuming
ethanndickson Nov 21, 2025
686e439
fix usage calc
ethanndickson Nov 21, 2025
8b68d16
fix usage calc
ethanndickson Nov 21, 2025
444c3ba
🤖 refactor: make countdown warning smaller and less intrusive
ethanndickson Nov 24, 2025
2f2d3eb
🤖 fix: prevent double-compaction when sending during active compaction
ethanndickson Nov 24, 2025
b184dcf
🤖 test: add comprehensive unit tests for shouldAutoCompact
ethanndickson Nov 24, 2025
b3e06da
🤖 feat: add auto-compaction configuration UI
ethanndickson Nov 20, 2025
02e99b4
fix merge conflict
ethanndickson Nov 21, 2025
dfabd45
cleanup
ethanndickson Nov 21, 2025
71eb2b2
🤖 fix: update tests for checkAutoCompaction API changes
ethanndickson Nov 24, 2025
2c6f833
Merge remote-tracking branch 'origin/main' into frontend-auto-compact…
ethanndickson Nov 25, 2025
e41a38b
refactor merge commit changes
ethanndickson Nov 25, 2025
1726268
fixup
ethanndickson Nov 25, 2025
b81219a
use pending model for auto compact check
ethanndickson Nov 25, 2025
9ac0931
fixup
ethanndickson Nov 25, 2025
da1e3ae
fix mobile
ethanndickson Nov 25, 2025
2456a85
call onMessageSent
ethanndickson Nov 25, 2025
9c4f2f5
Merge remote-tracking branch 'origin/frontend-auto-compaction-queue' …
ethanndickson Nov 25, 2025
0a5c938
fixup
ethanndickson Nov 25, 2025
63338f8
Merge remote-tracking branch 'origin/main' into auto-compaction-config
ethanndickson Nov 25, 2025
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
23 changes: 15 additions & 8 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,9 @@ import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds";
import { evictModelFromLRU } from "@/browser/hooks/useModelLRU";
import { QueuedMessage } from "./Messages/QueuedMessage";
import { CompactionWarning } from "./CompactionWarning";
import { shouldAutoCompact } from "@/browser/utils/compaction/autoCompactionCheck";
import { checkAutoCompaction } from "@/browser/utils/compaction/autoCompactionCheck";
import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
import { useAutoCompactionSettings } from "../hooks/useAutoCompactionSettings";
import { useSendMessageOptions } from "@/browser/hooks/useSendMessageOptions";

interface AIViewProps {
Expand Down Expand Up @@ -85,6 +86,8 @@ const AIViewInner: React.FC<AIViewProps> = ({
const workspaceUsage = useWorkspaceUsage(workspaceId);
const { options } = useProviderOptions();
const use1M = options.anthropic?.use1MContext ?? false;
const { enabled: autoCompactionEnabled, threshold: autoCompactionThreshold } =
useAutoCompactionSettings(workspaceId);
const handledModelErrorsRef = useRef<Set<string>>(new Set());

useEffect(() => {
Expand Down Expand Up @@ -337,12 +340,16 @@ const AIViewInner: React.FC<AIViewProps> = ({
// preventing context-length errors when switching from a large-context to smaller model.
const pendingModel = pendingSendOptions.model;

const autoCompactionCheck = pendingModel
? shouldAutoCompact(workspaceUsage, pendingModel, use1M)
: { shouldShowWarning: false, usagePercentage: 0, thresholdPercentage: 70 };
const autoCompactionResult = checkAutoCompaction(
workspaceUsage,
pendingModel,
use1M,
autoCompactionEnabled,
autoCompactionThreshold / 100
);

// Show warning when: shouldShowWarning flag is true AND not currently compacting
const shouldShowCompactionWarning = !isCompacting && autoCompactionCheck.shouldShowWarning;
const shouldShowCompactionWarning = !isCompacting && autoCompactionResult.shouldShowWarning;

// Note: We intentionally do NOT reset autoRetry when streams start.
// If user pressed the interrupt key, autoRetry stays false until they manually retry.
Expand Down Expand Up @@ -531,8 +538,8 @@ const AIViewInner: React.FC<AIViewProps> = ({
</div>
{shouldShowCompactionWarning && (
<CompactionWarning
usagePercentage={autoCompactionCheck.usagePercentage}
thresholdPercentage={autoCompactionCheck.thresholdPercentage}
usagePercentage={autoCompactionResult.usagePercentage}
thresholdPercentage={autoCompactionResult.thresholdPercentage}
/>
)}
<ChatInput
Expand All @@ -548,7 +555,7 @@ const AIViewInner: React.FC<AIViewProps> = ({
onEditLastUserMessage={() => void handleEditLastUserMessage()}
canInterrupt={canInterrupt}
onReady={handleChatInputReady}
autoCompactionCheck={autoCompactionCheck}
autoCompactionCheck={autoCompactionResult}
/>
</div>

Expand Down
65 changes: 65 additions & 0 deletions src/browser/components/RightSidebar/AutoCompactionSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import React from "react";
import { useAutoCompactionSettings } from "@/browser/hooks/useAutoCompactionSettings";
import { useClampedNumberInput } from "@/browser/hooks/useClampedNumberInput";
import {
AUTO_COMPACTION_THRESHOLD_MIN,
AUTO_COMPACTION_THRESHOLD_MAX,
} from "@/common/constants/ui";
import { TooltipWrapper, Tooltip, HelpIndicator } from "../Tooltip";

interface AutoCompactionSettingsProps {
workspaceId: string;
}

export const AutoCompactionSettings: React.FC<AutoCompactionSettingsProps> = ({ workspaceId }) => {
const { enabled, setEnabled, threshold, setThreshold } = useAutoCompactionSettings(workspaceId);
const { localValue, handleChange, handleBlur } = useClampedNumberInput(
threshold,
setThreshold,
AUTO_COMPACTION_THRESHOLD_MIN,
AUTO_COMPACTION_THRESHOLD_MAX
);

return (
<div data-testid="auto-compaction-settings" className="mb-6">
<div className="flex items-baseline justify-between">
{/* Left side: checkbox + label + tooltip */}
<div className="flex items-baseline gap-1">
<label className="text-foreground flex cursor-pointer items-baseline gap-1.5 font-medium select-none hover:text-white">
<input
type="checkbox"
checked={enabled}
onChange={(e) => setEnabled(e.target.checked)}
className="cursor-pointer"
/>
Auto-Compaction
</label>
<TooltipWrapper inline>
<HelpIndicator>?</HelpIndicator>
<Tooltip className="tooltip" align="center" width="auto">
Automatically compact conversation history when context usage reaches the threshold
</Tooltip>
</TooltipWrapper>
</div>

{/* Right side: input + % symbol */}
<div className="flex items-baseline gap-0.5">
<input
type="number"
min={AUTO_COMPACTION_THRESHOLD_MIN}
max={AUTO_COMPACTION_THRESHOLD_MAX}
step={5}
maxLength={2}
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
disabled={!enabled}
className="text-muted w-9 [appearance:textfield] border-none bg-transparent text-right text-xs outline-none disabled:cursor-not-allowed disabled:opacity-40 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
aria-label="Auto-compaction threshold percentage"
/>
<span className="text-muted text-xs">%</span>
</div>
</div>
</div>
);
};
3 changes: 3 additions & 0 deletions src/browser/components/RightSidebar/CostsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useProviderOptions } from "@/browser/hooks/useProviderOptions";
import { supports1MContext } from "@/common/utils/ai/models";
import { TOKEN_COMPONENT_COLORS } from "@/common/utils/tokens/tokenMeterUtils";
import { ConsumerBreakdown } from "./ConsumerBreakdown";
import { AutoCompactionSettings } from "./AutoCompactionSettings";

// Format token display - show k for thousands with 1 decimal
const formatTokens = (tokens: number) =>
Expand Down Expand Up @@ -231,6 +232,8 @@ const CostsTabComponent: React.FC<CostsTabProps> = ({ workspaceId }) => {
</div>
)}

{hasUsageData && <AutoCompactionSettings workspaceId={workspaceId} />}

{hasUsageData && (
<div data-testid="cost-section" className="mb-6">
<div className="flex flex-col gap-3">
Expand Down
40 changes: 40 additions & 0 deletions src/browser/hooks/useAutoCompactionSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { usePersistedState } from "@/browser/hooks/usePersistedState";
import {
getAutoCompactionEnabledKey,
getAutoCompactionThresholdKey,
} from "@/common/constants/storage";
import { DEFAULT_AUTO_COMPACTION_THRESHOLD_PERCENT } from "@/common/constants/ui";

export interface AutoCompactionSettings {
/** Whether auto-compaction is enabled for this workspace */
enabled: boolean;
/** Update enabled state */
setEnabled: (value: boolean) => void;
/** Current threshold percentage (50-90) */
threshold: number;
/** Update threshold percentage (will be clamped to 50-90 range by UI) */
setThreshold: (value: number) => void;
}

/**
* Custom hook for auto-compaction settings per workspace.
* Persists both enabled state and threshold percentage to localStorage.
*
* @param workspaceId - Workspace identifier
* @returns Settings object with getters and setters
*/
export function useAutoCompactionSettings(workspaceId: string): AutoCompactionSettings {
const [enabled, setEnabled] = usePersistedState<boolean>(
getAutoCompactionEnabledKey(workspaceId),
true,
{ listener: true }
);

const [threshold, setThreshold] = usePersistedState<number>(
getAutoCompactionThresholdKey(workspaceId),
DEFAULT_AUTO_COMPACTION_THRESHOLD_PERCENT,
{ listener: true }
);

return { enabled, setEnabled, threshold, setThreshold };
}
56 changes: 56 additions & 0 deletions src/browser/hooks/useClampedNumberInput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from "react";

/**
* Hook for number input with local state, validation, and clamping on blur.
* Prevents typing interruption while ensuring valid persisted values.
*
* @param persistedValue - Current value from persistence layer
* @param setPersisted - Function to update persisted value
* @param min - Minimum allowed value
* @param max - Maximum allowed value
* @returns Object with localValue, handleChange, and handleBlur
*/
export function useClampedNumberInput(
persistedValue: number,
setPersisted: (value: number) => void,
min: number,
max: number
) {
const [localValue, setLocalValue] = React.useState(persistedValue.toString());

// Sync local state when persisted value changes (e.g., from other tabs)
React.useEffect(() => {
setLocalValue(persistedValue.toString());
}, [persistedValue]);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const input = e.target.value;
// Allow empty or valid partial numbers (1-3 digits for typical use)
if (input === "" || /^\d{1,3}$/.test(input)) {
setLocalValue(input);
}
};

const handleBlur = () => {
const num = parseInt(localValue);

if (localValue === "" || isNaN(num)) {
// Invalid input - revert to persisted value
setLocalValue(persistedValue.toString());
} else if (num < min) {
// Below minimum - clamp to min
setPersisted(min);
setLocalValue(min.toString());
} else if (num > max) {
// Above maximum - clamp to max
setPersisted(max);
setLocalValue(max.toString());
} else {
// Valid - persist the value
setPersisted(num);
setLocalValue(num.toString());
}
};

return { localValue, handleChange, handleBlur };
}
Loading