Compare commits

...

2 Commits

Author SHA1 Message Date
abeatrix 66b1b86d04 feat: add draggable threshold adjustment to AutoCondenseMarker
Add interactive drag functionality to adjust the auto-condense threshold marker. This includes:

- New props for threshold change callback and progress bar reference
- Mouse event handlers for drag operations with global listeners
- Separate wider hit area (12px) for easier interaction and visible 1px marker line
- Visual feedback during dragging (opacity change and bold label)
- Percentage display shown during drag interaction

This allows users to dynamically adjust the threshold by dragging the marker on the progress bar.
2025-11-20 15:51:08 -08:00
abeatrix 3633608c3f feat: integrate auto condense feature flag with user settings
Add feature flag support for AUTO_CONDENSE to allow independent control over the auto condense feature through both user settings and feature flags for enabling auto condense threshold for testing.

Changes:
- Add AUTO_CONDENSE feature flag definition and default value (dev-only)
- Create getUseAutoCondenseEnabled() method in FeatureFlagsService
- Update useAutoCondense state structure to track both user preference and feature flag status
- Modify ExtensionState type to use ClineFeatureSetting for useAutoCondense
- Update state initialization to use feature flag as fallback default value
2025-11-20 15:40:46 -08:00
11 changed files with 95 additions and 23 deletions
+4 -2
View File
@@ -860,7 +860,6 @@ export class Controller {
const mode = this.stateManager.getGlobalSettingsKey("mode")
const strictPlanModeEnabled = this.stateManager.getGlobalSettingsKey("strictPlanModeEnabled")
const yoloModeToggled = this.stateManager.getGlobalSettingsKey("yoloModeToggled")
const useAutoCondense = this.stateManager.getGlobalSettingsKey("useAutoCondense")
const userInfo = this.stateManager.getGlobalStateKey("userInfo")
const mcpMarketplaceEnabled = this.stateManager.getGlobalStateKey("mcpMarketplaceEnabled")
const mcpDisplayMode = this.stateManager.getGlobalStateKey("mcpDisplayMode")
@@ -935,7 +934,10 @@ export class Controller {
mode,
strictPlanModeEnabled,
yoloModeToggled,
useAutoCondense,
useAutoCondense: {
user: this.stateManager.getGlobalSettingsKey("useAutoCondense"),
featureFlag: featureFlagsService.getUseAutoCondenseEnabled(),
},
userInfo,
mcpMarketplaceEnabled,
mcpDisplayMode,
+2 -1
View File
@@ -3,6 +3,7 @@ import { GlobalStateAndSettings, LocalState, SecretKey, Secrets } from "@shared/
import { ExtensionContext } from "vscode"
import { Controller } from "@/core/controller"
import { getHooksEnabledSafe } from "@/core/hooks/hooks-utils"
import { featureFlagsService } from "@/services/feature-flags"
import { DEFAULT_AUTO_APPROVAL_SETTINGS } from "@/shared/AutoApprovalSettings"
import { DEFAULT_BROWSER_SETTINGS } from "@/shared/BrowserSettings"
import { ClineRulesToggles } from "@/shared/cline-rules"
@@ -636,7 +637,7 @@ export async function readGlobalStateFromDisk(context: ExtensionContext): Promis
dictationSettings: { ...DEFAULT_DICTATION_SETTINGS, ...dictationSettings },
strictPlanModeEnabled: strictPlanModeEnabled ?? true,
yoloModeToggled: yoloModeToggled ?? false,
useAutoCondense: useAutoCondense ?? false,
useAutoCondense: useAutoCondense ?? featureFlagsService.getUseAutoCondenseEnabled(),
clineWebToolsEnabled: clineWebToolsEnabled ?? true,
isNewUser: isNewUser ?? true,
welcomeViewCompleted,
@@ -97,6 +97,10 @@ export class FeatureFlagsService {
return this.getBooleanFlagEnabled(FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS)
}
public getUseAutoCondenseEnabled(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.AUTO_CONDENSE)
}
public isResponseApiEnabled(): boolean {
return this.getBooleanFlagEnabled(FeatureFlag.OPENAI_NATIVE_RESPONSE_API)
}
+1 -1
View File
@@ -86,7 +86,7 @@ export interface ExtensionState {
mcpResponsesCollapsed?: boolean
strictPlanModeEnabled?: boolean
yoloModeToggled?: boolean
useAutoCondense?: boolean
useAutoCondense?: ClineFeatureSetting
focusChainSettings: FocusChainSettings
dictationSettings: DictationSettings
customPrompt?: string
@@ -11,6 +11,7 @@ export enum FeatureFlag {
// Feature flag for showing the new onboarding flow or old welcome view.
ONBOARDING_MODELS = "onboarding_models",
OPENAI_NATIVE_RESPONSE_API = "openai_native_response_api",
AUTO_CONDENSE = "auto_condense",
}
export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPayload>> = {
@@ -19,6 +20,7 @@ export const FeatureFlagDefaultValue: Partial<Record<FeatureFlag, FeatureFlagPay
[FeatureFlag.NATIVE_TOOL_CALLS_NEXT_GEN_MODELS]: process.env.IS_DEV === "true",
[FeatureFlag.ONBOARDING_MODELS]: process.env.E2E_TEST === "true" ? { models: {} } : undefined,
[FeatureFlag.OPENAI_NATIVE_RESPONSE_API]: process.env.IS_DEV === "true",
[FeatureFlag.AUTO_CONDENSE]: process.env.IS_DEV === "true",
}
export const FEATURE_FLAGS = Object.values(FeatureFlag)
@@ -1,21 +1,25 @@
import { cn } from "@heroui/react"
import React, { useEffect, useMemo, useRef, useState } from "react"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
export const AutoCondenseMarker: React.FC<{
threshold: number
usage: number
isContextWindowHoverOpen?: boolean
shouldAnimate?: boolean
}> = ({ threshold, usage, isContextWindowHoverOpen, shouldAnimate = false }) => {
onThresholdChange?: (newThreshold: number) => void
progressBarRef?: React.RefObject<HTMLDivElement>
}> = ({ threshold, usage, isContextWindowHoverOpen, shouldAnimate = false, onThresholdChange, progressBarRef }) => {
const [isAnimating, setIsAnimating] = useState(false)
const [animatedPosition, setAnimatedPosition] = useState(0)
const [showPercentageAfterAnimation, setShowPercentageAfterAnimation] = useState(false)
const [isFadingOut, setIsFadingOut] = useState(false)
const [isDragging, setIsDragging] = useState(false)
// Refs to store animation frame and timeout IDs for cleanup
const animationFrameRef = useRef<number | null>(null)
const fadeOutTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const hideTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const isDraggingRef = useRef(false)
// Animation effect when shouldAnimate prop changes (initial load)
useEffect(() => {
@@ -80,6 +84,44 @@ export const AutoCondenseMarker: React.FC<{
return cleanup
}, [shouldAnimate, threshold])
// Drag handlers
const handleMouseDown = useCallback((e: React.MouseEvent) => {
e.preventDefault()
e.stopPropagation()
isDraggingRef.current = true
setIsDragging(true)
}, [])
const handleMouseMove = useCallback(
(e: MouseEvent) => {
if (!isDraggingRef.current || !progressBarRef?.current || !onThresholdChange) {
return
}
const rect = progressBarRef.current.getBoundingClientRect()
const clickX = e.clientX - rect.left
const percentage = Math.max(0, Math.min(1, clickX / rect.width))
const newThreshold = Math.round(percentage * 100) / 100
onThresholdChange(newThreshold)
},
[progressBarRef, onThresholdChange],
)
const handleMouseUp = useCallback(() => {
isDraggingRef.current = false
setIsDragging(false)
}, [])
// Add global mouse event listeners for dragging - always listen but only act when dragging
useEffect(() => {
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
return () => {
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
}, [handleMouseMove, handleMouseUp])
// The marker position is calculated based on the threshold percentage
// It goes over the progress bar to indicate where the auto-condense will trigger
// and it should highlight from what the current percentage (usage) is
@@ -103,21 +145,34 @@ export const AutoCondenseMarker: React.FC<{
return (
<div className="flex-1" id="auto-condense-threshold-marker">
{/* Invisible wider hit area for easier dragging */}
<div
className={cn(
"absolute top-0 bottom-0 h-full cursor-pointer pointer-events-none z-10 bg-button-background shadow-lg w-1",
{
"transition-all duration-75": !isAnimating,
},
)}
className={cn("absolute top-0 bottom-0 h-full cursor-ew-resize z-10", {
"pointer-events-auto": onThresholdChange,
"pointer-events-none": !onThresholdChange,
})}
onMouseDown={onThresholdChange ? handleMouseDown : undefined}
style={{
left: marker.start,
width: "12px",
transform: `translateX(-6px)`, // Center the hit area on the marker
}}
/>
{/* Visible 1px line */}
<div
className={cn("absolute top-0 bottom-0 h-full pointer-events-none z-10 bg-button-background shadow-lg w-1", {
"transition-all duration-75": !isAnimating && !isDragging,
"opacity-80": isDragging,
})}
style={{
left: marker.start,
transform: isAnimating ? `translateX(${animatedPosition - threshold * 100}%)` : "translateX(0)",
}}>
{(isContextWindowHoverOpen || isAnimating || showPercentageAfterAnimation) && (
{(isContextWindowHoverOpen || isAnimating || showPercentageAfterAnimation || isDragging) && (
<div
className={cn("absolute -top-4 -left-1 text-button-background font-mono text-xs", {
"opacity-0": isFadingOut,
"opacity-0": isFadingOut && !isDragging,
"font-bold": isDragging,
})}>
{marker.label}%
</div>
@@ -19,7 +19,7 @@ interface ContextWindowInfoProps {
}
interface ContextWindowProgressProps extends ContextWindowInfoProps {
useAutoCondense: boolean
useAutoCondense?: boolean
lastApiReqTotalTokens?: number
contextWindow?: number
autoCondenseThreshold?: number
@@ -94,6 +94,12 @@ const ContextWindow: React.FC<ContextWindowProgressProps> = ({
updateSetting("autoCondenseThreshold", newThreshold)
}, [])
const handleThresholdChange = useCallback((newThreshold: number) => {
setConfirmationNeeded(false)
setThreshold(newThreshold)
updateSetting("autoCondenseThreshold", newThreshold)
}, [])
const handleCompactClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault()
@@ -246,6 +252,8 @@ const ContextWindow: React.FC<ContextWindowProgressProps> = ({
{useAutoCondense && (
<AutoCondenseMarker
isContextWindowHoverOpen={isOpened}
onThresholdChange={handleThresholdChange}
progressBarRef={progressBarRef}
shouldAnimate={shouldAnimateMarker}
threshold={threshold}
usage={tokenData.percentage}
@@ -126,14 +126,12 @@ export const ContextWindowSummary: React.FC<TaskContextWindowButtonsProps> = ({
<AccordionItem
isExpanded={expandedSections.has("threshold")}
onToggle={(event) => toggleSection("threshold", event)}
title="Auto Condense Threshold"
title="Compact Threshold"
value={<span className="text-muted-foreground">{`${(autoCompactThreshold * 100).toFixed(0)}%`}</span>}>
<div className="space-y-1">
<p className="text-xs leading-relaxed text-white">
Click on the context window bar to set a new threshold.
</p>
<p className="text-xs leading-relaxed mt-0 mb-0">
When the context window usage exceeds this threshold, the task will be automatically condensed.
When the context window usage exceeds this threshold, the task will be automatically condensed. Click
on the context window bar to set a new threshold.
</p>
</div>
</AccordionItem>
@@ -59,6 +59,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
expandTaskHeader: isTaskExpanded,
setExpandTaskHeader: setIsTaskExpanded,
environment,
useAutoCondense,
} = useExtensionState()
const [isHighlightedTextExpanded, setIsHighlightedTextExpanded] = useState(false)
@@ -207,7 +208,8 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
onSendMessage={onSendMessage}
tokensIn={tokensIn}
tokensOut={tokensOut}
useAutoCondense={false} // Disable auto-condense configuration in UI for now
// Requires both user setting and feature flag to be enabled
useAutoCondense={useAutoCondense?.user && useAutoCondense.featureFlag}
/>
<TaskTimeline messages={clineMessages} onBlockClick={onScrollToMessage} />
@@ -338,7 +338,7 @@ const FeatureSettingsSection = ({ renderSectionHeader }: FeatureSettingsSectionP
)}
<div style={{ marginTop: 10 }}>
<VSCodeCheckbox
checked={useAutoCondense}
checked={useAutoCondense?.user}
onChange={(e: any) => {
const checked = e.target.checked === true
updateSetting("useAutoCondense", checked)
@@ -221,7 +221,7 @@ export const ExtensionStateContextProvider: React.FC<{
strictPlanModeEnabled: false,
yoloModeToggled: false,
customPrompt: undefined,
useAutoCondense: false,
useAutoCondense: { user: false, featureFlag: false },
autoCondenseThreshold: undefined,
favoritedModelIds: [],
lastDismissedInfoBannerVersion: 0,