diff --git a/src/browser/components/AIView.tsx b/src/browser/components/AIView.tsx index fad0738bf..1ce697f21 100644 --- a/src/browser/components/AIView.tsx +++ b/src/browser/components/AIView.tsx @@ -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 { @@ -85,6 +86,8 @@ const AIViewInner: React.FC = ({ const workspaceUsage = useWorkspaceUsage(workspaceId); const { options } = useProviderOptions(); const use1M = options.anthropic?.use1MContext ?? false; + const { enabled: autoCompactionEnabled, threshold: autoCompactionThreshold } = + useAutoCompactionSettings(workspaceId); const handledModelErrorsRef = useRef>(new Set()); useEffect(() => { @@ -337,12 +340,16 @@ const AIViewInner: React.FC = ({ // 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. @@ -531,8 +538,8 @@ const AIViewInner: React.FC = ({ {shouldShowCompactionWarning && ( )} = ({ onEditLastUserMessage={() => void handleEditLastUserMessage()} canInterrupt={canInterrupt} onReady={handleChatInputReady} - autoCompactionCheck={autoCompactionCheck} + autoCompactionCheck={autoCompactionResult} /> diff --git a/src/browser/components/RightSidebar/AutoCompactionSettings.tsx b/src/browser/components/RightSidebar/AutoCompactionSettings.tsx new file mode 100644 index 000000000..518c85ce4 --- /dev/null +++ b/src/browser/components/RightSidebar/AutoCompactionSettings.tsx @@ -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 = ({ workspaceId }) => { + const { enabled, setEnabled, threshold, setThreshold } = useAutoCompactionSettings(workspaceId); + const { localValue, handleChange, handleBlur } = useClampedNumberInput( + threshold, + setThreshold, + AUTO_COMPACTION_THRESHOLD_MIN, + AUTO_COMPACTION_THRESHOLD_MAX + ); + + return ( +
+
+ {/* Left side: checkbox + label + tooltip */} +
+ + + ? + + Automatically compact conversation history when context usage reaches the threshold + + +
+ + {/* Right side: input + % symbol */} +
+ + % +
+
+
+ ); +}; diff --git a/src/browser/components/RightSidebar/CostsTab.tsx b/src/browser/components/RightSidebar/CostsTab.tsx index aeae58152..f5053c8d5 100644 --- a/src/browser/components/RightSidebar/CostsTab.tsx +++ b/src/browser/components/RightSidebar/CostsTab.tsx @@ -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) => @@ -231,6 +232,8 @@ const CostsTabComponent: React.FC = ({ workspaceId }) => { )} + {hasUsageData && } + {hasUsageData && (
diff --git a/src/browser/hooks/useAutoCompactionSettings.ts b/src/browser/hooks/useAutoCompactionSettings.ts new file mode 100644 index 000000000..3a5b436ed --- /dev/null +++ b/src/browser/hooks/useAutoCompactionSettings.ts @@ -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( + getAutoCompactionEnabledKey(workspaceId), + true, + { listener: true } + ); + + const [threshold, setThreshold] = usePersistedState( + getAutoCompactionThresholdKey(workspaceId), + DEFAULT_AUTO_COMPACTION_THRESHOLD_PERCENT, + { listener: true } + ); + + return { enabled, setEnabled, threshold, setThreshold }; +} diff --git a/src/browser/hooks/useClampedNumberInput.ts b/src/browser/hooks/useClampedNumberInput.ts new file mode 100644 index 000000000..998ff0e97 --- /dev/null +++ b/src/browser/hooks/useClampedNumberInput.ts @@ -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) => { + 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 }; +} diff --git a/src/browser/utils/compaction/autoCompactionCheck.test.ts b/src/browser/utils/compaction/autoCompactionCheck.test.ts index 8e1d26f3d..2b656cc1f 100644 --- a/src/browser/utils/compaction/autoCompactionCheck.test.ts +++ b/src/browser/utils/compaction/autoCompactionCheck.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test"; -import { shouldAutoCompact } from "./autoCompactionCheck"; +import { checkAutoCompaction } from "./autoCompactionCheck"; import type { WorkspaceUsageState } from "@/browser/stores/WorkspaceStore"; import type { ChatUsageDisplay } from "@/common/utils/tokens/usageAggregator"; import { KNOWN_MODELS } from "@/common/constants/knownModels"; @@ -43,14 +43,14 @@ const createMockUsage = ( return { usageHistory, totalTokens: 0 }; }; -describe("shouldAutoCompact", () => { +describe("checkAutoCompaction", () => { const SONNET_MAX_TOKENS = 200_000; const SONNET_70_PERCENT = SONNET_MAX_TOKENS * 0.7; // 140,000 const SONNET_60_PERCENT = SONNET_MAX_TOKENS * 0.6; // 120,000 describe("Basic Functionality", () => { test("returns false when no usage data (first message)", () => { - const result = shouldAutoCompact(undefined, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(undefined, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(0); @@ -59,7 +59,7 @@ describe("shouldAutoCompact", () => { test("returns false when usage history is empty", () => { const usage: WorkspaceUsageState = { usageHistory: [], totalTokens: 0 }; - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(0); @@ -68,7 +68,7 @@ describe("shouldAutoCompact", () => { test("returns false when model has no max_input_tokens (unknown model)", () => { const usage = createMockUsage(50_000); - const result = shouldAutoCompact(usage, "unknown-model", false); + const result = checkAutoCompaction(usage, "unknown-model", false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(0); @@ -77,7 +77,7 @@ describe("shouldAutoCompact", () => { test("returns false when usage is low (10%)", () => { const usage = createMockUsage(20_000); // 10% of 200k - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(10); @@ -86,7 +86,7 @@ describe("shouldAutoCompact", () => { test("returns true at warning threshold (60% with default 10% advance)", () => { const usage = createMockUsage(SONNET_60_PERCENT); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(60); @@ -95,7 +95,7 @@ describe("shouldAutoCompact", () => { test("returns true at compaction threshold (70%)", () => { const usage = createMockUsage(SONNET_70_PERCENT); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(70); @@ -104,7 +104,7 @@ describe("shouldAutoCompact", () => { test("returns true above threshold (80%)", () => { const usage = createMockUsage(160_000); // 80% of 200k - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(80); @@ -115,7 +115,7 @@ describe("shouldAutoCompact", () => { describe("Usage Calculation (Critical for infinite loop fix)", () => { test("uses last usage entry tokens, not cumulative sum", () => { const usage = createMockUsage(10_000); // Only 5% of context - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); // Should be 5%, not counting historical expect(result.usagePercentage).toBe(5); @@ -126,7 +126,7 @@ describe("shouldAutoCompact", () => { // Scenario: After compaction, historical = 70K, recent = 5K // Should calculate based on 5K (2.5%), not 75K (37.5%) const usage = createMockUsage(5_000, 70_000); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.usagePercentage).toBe(2.5); expect(result.shouldShowWarning).toBe(false); @@ -148,7 +148,7 @@ describe("shouldAutoCompact", () => { totalTokens: 0, }; - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); // Total: 10k + 5k + 2k + 3k + 1k = 21k tokens = 10.5% expect(result.usagePercentage).toBe(10.5); @@ -158,7 +158,7 @@ describe("shouldAutoCompact", () => { describe("1M Context Mode", () => { test("uses 1M tokens when use1M=true and model supports it (Sonnet 4)", () => { const usage = createMockUsage(600_000); // 60% of 1M - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, true); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, true, true); expect(result.usagePercentage).toBe(60); expect(result.shouldShowWarning).toBe(true); @@ -166,7 +166,7 @@ describe("shouldAutoCompact", () => { test("uses 1M tokens for Sonnet with use1M=true (model is claude-sonnet-4-5)", () => { const usage = createMockUsage(700_000); // 70% of 1M - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, true); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, true, true); expect(result.usagePercentage).toBe(70); expect(result.shouldShowWarning).toBe(true); @@ -174,7 +174,7 @@ describe("shouldAutoCompact", () => { test("uses standard max_input_tokens when use1M=false", () => { const usage = createMockUsage(140_000); // 70% of 200k - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.usagePercentage).toBe(70); expect(result.shouldShowWarning).toBe(true); @@ -183,7 +183,7 @@ describe("shouldAutoCompact", () => { test("ignores use1M for models that don't support it (GPT)", () => { const usage = createMockUsage(100_000, undefined, KNOWN_MODELS.GPT_MINI.id); // GPT Mini has 272k context, so 100k = 36.76% - const result = shouldAutoCompact(usage, KNOWN_MODELS.GPT_MINI.id, true); + const result = checkAutoCompaction(usage, KNOWN_MODELS.GPT_MINI.id, true, true); // Should use standard 272k, not 1M (use1M ignored for GPT) expect(result.usagePercentage).toBeCloseTo(36.76, 1); @@ -194,7 +194,7 @@ describe("shouldAutoCompact", () => { describe("Edge Cases", () => { test("empty usageHistory array returns safe defaults", () => { const usage: WorkspaceUsageState = { usageHistory: [], totalTokens: 0 }; - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(0); @@ -203,7 +203,7 @@ describe("shouldAutoCompact", () => { test("single entry in usageHistory works correctly", () => { const usage = createMockUsage(140_000); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(70); @@ -211,7 +211,7 @@ describe("shouldAutoCompact", () => { test("custom threshold parameter (80%)", () => { const usage = createMockUsage(140_000); // 70% of context - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false, 0.8); // 80% threshold + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true, 0.8); // 80% threshold // At 70%, should NOT show warning for 80% threshold (needs 70% advance = 10%) expect(result.shouldShowWarning).toBe(true); // 70% >= (80% - 10% = 70%) @@ -221,7 +221,7 @@ describe("shouldAutoCompact", () => { test("custom warning advance (5% instead of 10%)", () => { const usage = createMockUsage(130_000); // 65% of context - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false, 0.7, 5); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true, 0.7, 5); // At 65%, should show warning with 5% advance (70% - 5% = 65%) expect(result.shouldShowWarning).toBe(true); @@ -244,7 +244,7 @@ describe("shouldAutoCompact", () => { totalTokens: 0, }; - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(false); expect(result.usagePercentage).toBe(0); @@ -252,7 +252,7 @@ describe("shouldAutoCompact", () => { test("handles usage at exactly 100% of context", () => { const usage = createMockUsage(SONNET_MAX_TOKENS); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(100); @@ -261,7 +261,7 @@ describe("shouldAutoCompact", () => { test("handles usage beyond 100% of context", () => { const usage = createMockUsage(SONNET_MAX_TOKENS + 50_000); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.shouldShowWarning).toBe(true); expect(result.usagePercentage).toBe(125); @@ -284,14 +284,14 @@ describe("shouldAutoCompact", () => { for (const { tokens, expectedPercent } of testCases) { const usage = createMockUsage(tokens); - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.usagePercentage).toBe(expectedPercent); } }); test("handles fractional percentages correctly", () => { const usage = createMockUsage(123_456); // 61.728% - const result = shouldAutoCompact(usage, KNOWN_MODELS.SONNET.id, false); + const result = checkAutoCompaction(usage, KNOWN_MODELS.SONNET.id, false, true); expect(result.usagePercentage).toBeCloseTo(61.728, 2); expect(result.shouldShowWarning).toBe(true); // Above 60% diff --git a/src/browser/utils/compaction/autoCompactionCheck.ts b/src/browser/utils/compaction/autoCompactionCheck.ts index c532395c7..f25a64a28 100644 --- a/src/browser/utils/compaction/autoCompactionCheck.ts +++ b/src/browser/utils/compaction/autoCompactionCheck.ts @@ -18,6 +18,7 @@ import type { WorkspaceUsageState } from "@/browser/stores/WorkspaceStore"; import { getModelStats } from "@/common/utils/tokens/modelStats"; import { supports1MContext } from "@/common/utils/ai/models"; +import { DEFAULT_AUTO_COMPACTION_THRESHOLD } from "@/common/constants/ui"; export interface AutoCompactionCheckResult { shouldShowWarning: boolean; @@ -25,10 +26,6 @@ export interface AutoCompactionCheckResult { thresholdPercentage: number; } -// Auto-compaction threshold (0.7 = 70%) -// TODO: Make this configurable via settings -const AUTO_COMPACTION_THRESHOLD = 0.7; - // Show warning this many percentage points before threshold const WARNING_ADVANCE_PERCENT = 10; @@ -40,23 +37,26 @@ const WARNING_ADVANCE_PERCENT = 10; * preventing infinite compaction loops after the first compaction completes. * * @param usage - Current workspace usage state (from useWorkspaceUsage) - * @param model - Current model string + * @param model - Current model string (optional - returns safe default if not provided) * @param use1M - Whether 1M context is enabled + * @param enabled - Whether auto-compaction is enabled for this workspace * @param threshold - Usage percentage threshold (0.0-1.0, default 0.7 = 70%) * @param warningAdvancePercent - Show warning this many percentage points before threshold (default 10) * @returns Check result with warning flag and usage percentage */ -export function shouldAutoCompact( +export function checkAutoCompaction( usage: WorkspaceUsageState | undefined, - model: string, + model: string | null, use1M: boolean, - threshold: number = AUTO_COMPACTION_THRESHOLD, + enabled: boolean, + threshold: number = DEFAULT_AUTO_COMPACTION_THRESHOLD, warningAdvancePercent: number = WARNING_ADVANCE_PERCENT ): AutoCompactionCheckResult { const thresholdPercentage = threshold * 100; - // No usage data yet - safe default (don't trigger on first message) - if (!usage || usage.usageHistory.length === 0) { + // Short-circuit if auto-compaction is disabled + // Or if no usage data yet + if (!enabled || !model || !usage || usage.usageHistory.length === 0) { return { shouldShowWarning: false, usagePercentage: 0, @@ -67,6 +67,7 @@ export function shouldAutoCompact( // Determine max tokens for this model const modelStats = getModelStats(model); const maxTokens = use1M && supports1MContext(model) ? 1_000_000 : modelStats?.max_input_tokens; + const lastUsage = usage.usageHistory[usage.usageHistory.length - 1]; // No max tokens known - safe default (can't calculate percentage) if (!maxTokens) { @@ -77,16 +78,6 @@ export function shouldAutoCompact( }; } - // Use last usage entry to calculate current context size (matches UI display) - const lastUsage = usage.usageHistory[usage.usageHistory.length - 1]; - if (!lastUsage) { - return { - shouldShowWarning: false, - usagePercentage: 0, - thresholdPercentage, - }; - } - const currentContextTokens = lastUsage.input.tokens + lastUsage.cached.tokens + diff --git a/src/common/constants/storage.ts b/src/common/constants/storage.ts index bfaa80a16..e1108c0a3 100644 --- a/src/common/constants/storage.ts +++ b/src/common/constants/storage.ts @@ -153,6 +153,22 @@ export function getReviewSearchStateKey(workspaceId: string): string { return `reviewSearchState:${workspaceId}`; } +/** + * Get the localStorage key for auto-compaction enabled preference per workspace + * Format: "autoCompaction:enabled:{workspaceId}" + */ +export function getAutoCompactionEnabledKey(workspaceId: string): string { + return `autoCompaction:enabled:${workspaceId}`; +} + +/** + * Get the localStorage key for auto-compaction threshold percentage per workspace + * Format: "autoCompaction:threshold:{workspaceId}" + */ +export function getAutoCompactionThresholdKey(workspaceId: string): string { + return `autoCompaction:threshold:${workspaceId}`; +} + /** * List of workspace-scoped key functions that should be copied on fork and deleted on removal */ @@ -166,6 +182,8 @@ const PERSISTENT_WORKSPACE_KEY_FUNCTIONS: Array<(workspaceId: string) => string> getReviewExpandStateKey, getFileTreeExpandStateKey, getReviewSearchStateKey, + getAutoCompactionEnabledKey, + getAutoCompactionThresholdKey, ]; /** diff --git a/src/common/constants/ui.ts b/src/common/constants/ui.ts index d038b8fef..f4b7437a5 100644 --- a/src/common/constants/ui.ts +++ b/src/common/constants/ui.ts @@ -10,6 +10,23 @@ */ export const COMPACTED_EMOJI = "📦"; +/** + * Auto-compaction threshold bounds (percentage) + * Too low risks frequent interruptions; too high risks hitting context limits + */ +export const AUTO_COMPACTION_THRESHOLD_MIN = 50; +export const AUTO_COMPACTION_THRESHOLD_MAX = 90; + +/** + * Default auto-compaction threshold percentage (50-90 range) + * Applied when creating new workspaces + */ +export const DEFAULT_AUTO_COMPACTION_THRESHOLD_PERCENT = 70; + +/** + * Default threshold as decimal for calculations (0.7 = 70%) + */ +export const DEFAULT_AUTO_COMPACTION_THRESHOLD = DEFAULT_AUTO_COMPACTION_THRESHOLD_PERCENT / 100; /** * Duration (ms) to show "copied" feedback after copying to clipboard */