Skip to content

Commit 4d31e9e

Browse files
ammar-agentmux
andcommitted
🤖 refactor: unify ThresholdSlider for horizontal and vertical orientations
- ThresholdSlider component now supports both orientations - Removed 50% minimum threshold - allow 0-90% (100% = disabled) - Added VerticalThresholdSlider to VerticalTokenMeter - Both sliders share the same per-model threshold state - Use native title tooltips for hover feedback Co-authored-by: mux <mux@coder.com>
1 parent 28a6667 commit 4d31e9e

File tree

3 files changed

+141
-72
lines changed

3 files changed

+141
-72
lines changed

‎src/browser/components/RightSidebar/ThresholdSlider.tsx‎

Lines changed: 124 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,14 @@ export interface AutoCompactionConfig {
1111
setThreshold: (threshold: number) => void;
1212
}
1313

14-
interface HorizontalThresholdSliderProps {
14+
interface ThresholdSliderProps {
1515
config: AutoCompactionConfig;
16+
orientation: "horizontal" | "vertical";
1617
}
1718

1819
// ----- Constants -----
1920

20-
/** Threshold at which we consider auto-compaction disabled (dragged all the way right) */
21+
/** Threshold at which we consider auto-compaction disabled (dragged all the way to end) */
2122
const DISABLE_THRESHOLD = 100;
2223

2324
/** Size of the triangle markers in pixels */
@@ -26,27 +27,65 @@ const TRIANGLE_SIZE = 4;
2627
// ----- Subcomponents -----
2728

2829
/** CSS triangle pointing in specified direction */
29-
const Triangle: React.FC<{ direction: "up" | "down"; color: string }> = ({ direction, color }) => (
30-
<div
31-
style={{
32-
width: 0,
33-
height: 0,
34-
borderLeft: `${TRIANGLE_SIZE}px solid transparent`,
35-
borderRight: `${TRIANGLE_SIZE}px solid transparent`,
36-
...(direction === "down"
37-
? { borderTop: `${TRIANGLE_SIZE}px solid ${color}` }
38-
: { borderBottom: `${TRIANGLE_SIZE}px solid ${color}` }),
39-
}}
40-
/>
41-
);
30+
const Triangle: React.FC<{ direction: "up" | "down" | "left" | "right"; color: string }> = ({
31+
direction,
32+
color,
33+
}) => {
34+
const styles: React.CSSProperties = { width: 0, height: 0 };
35+
36+
if (direction === "up" || direction === "down") {
37+
styles.borderLeft = `${TRIANGLE_SIZE}px solid transparent`;
38+
styles.borderRight = `${TRIANGLE_SIZE}px solid transparent`;
39+
if (direction === "down") {
40+
styles.borderTop = `${TRIANGLE_SIZE}px solid ${color}`;
41+
} else {
42+
styles.borderBottom = `${TRIANGLE_SIZE}px solid ${color}`;
43+
}
44+
} else {
45+
styles.borderTop = `${TRIANGLE_SIZE}px solid transparent`;
46+
styles.borderBottom = `${TRIANGLE_SIZE}px solid transparent`;
47+
if (direction === "right") {
48+
styles.borderLeft = `${TRIANGLE_SIZE}px solid ${color}`;
49+
} else {
50+
styles.borderRight = `${TRIANGLE_SIZE}px solid ${color}`;
51+
}
52+
}
53+
54+
return <div style={styles} />;
55+
};
56+
57+
// ----- Shared utilities -----
4258

43-
// ----- Main component: HorizontalThresholdSlider -----
59+
/** Clamp and snap percentage to valid threshold values */
60+
const snapPercent = (raw: number): number => {
61+
const clamped = Math.max(AUTO_COMPACTION_THRESHOLD_MIN, Math.min(100, raw));
62+
return Math.round(clamped / 5) * 5;
63+
};
64+
65+
/** Apply threshold, handling the disable case */
66+
const applyThreshold = (pct: number, setThreshold: (v: number) => void): void => {
67+
setThreshold(pct >= DISABLE_THRESHOLD ? 100 : Math.min(pct, AUTO_COMPACTION_THRESHOLD_MAX));
68+
};
69+
70+
/** Get tooltip text based on threshold */
71+
const getTooltip = (threshold: number, orientation: "horizontal" | "vertical"): string => {
72+
const isEnabled = threshold < DISABLE_THRESHOLD;
73+
const direction = orientation === "horizontal" ? "left" : "up";
74+
return isEnabled
75+
? `Auto-compact at ${threshold}% · Drag to adjust (per-model)`
76+
: `Auto-compact disabled · Drag ${direction} to enable (per-model)`;
77+
};
78+
79+
// ----- Main component: ThresholdSlider -----
4480

4581
/**
46-
* A draggable threshold indicator for horizontal progress bars.
82+
* A draggable threshold indicator for progress bars (horizontal or vertical).
4783
*
48-
* Renders as a vertical line with triangle handles at the threshold position.
49-
* Drag left/right to adjust threshold. Drag to 100% to disable.
84+
* - Horizontal: Renders as a vertical line with up/down triangle handles.
85+
* Drag left/right to adjust threshold. Drag to 100% (right) to disable.
86+
*
87+
* - Vertical: Renders as a horizontal line with left/right triangle handles.
88+
* Drag up/down to adjust threshold. Drag to 100% (bottom) to disable.
5089
*
5190
* USAGE: Place as a sibling AFTER the progress bar, both inside a relative container.
5291
*
@@ -57,30 +96,30 @@ const Triangle: React.FC<{ direction: "up" | "down"; color: string }> = ({ direc
5796
* how Tailwind's JIT compiler or class application interacts with dynamically
5897
* rendered components in this context. Inline styles work reliably.
5998
*/
60-
export const HorizontalThresholdSlider: React.FC<HorizontalThresholdSliderProps> = ({ config }) => {
99+
export const ThresholdSlider: React.FC<ThresholdSliderProps> = ({ config, orientation }) => {
61100
const containerRef = useRef<HTMLDivElement>(null);
101+
const isHorizontal = orientation === "horizontal";
62102

63103
const handleMouseDown = (e: React.MouseEvent) => {
64104
e.preventDefault();
65105

66106
const rect = containerRef.current?.getBoundingClientRect();
67107
if (!rect) return;
68108

69-
const calcPercent = (clientX: number) => {
70-
const raw = ((clientX - rect.left) / rect.width) * 100;
71-
const clamped = Math.max(AUTO_COMPACTION_THRESHOLD_MIN, Math.min(100, raw));
72-
return Math.round(clamped / 5) * 5;
109+
const calcPercent = (clientX: number, clientY: number) => {
110+
if (isHorizontal) {
111+
return snapPercent(((clientX - rect.left) / rect.width) * 100);
112+
} else {
113+
// Vertical: top = low %, bottom = high %
114+
return snapPercent(((clientY - rect.top) / rect.height) * 100);
115+
}
73116
};
74117

75-
const applyThreshold = (pct: number) => {
76-
config.setThreshold(
77-
pct >= DISABLE_THRESHOLD ? 100 : Math.min(pct, AUTO_COMPACTION_THRESHOLD_MAX)
78-
);
79-
};
118+
const apply = (pct: number) => applyThreshold(pct, config.setThreshold);
80119

81-
applyThreshold(calcPercent(e.clientX));
120+
apply(calcPercent(e.clientX, e.clientY));
82121

83-
const onMove = (ev: MouseEvent) => applyThreshold(calcPercent(ev.clientX));
122+
const onMove = (ev: MouseEvent) => apply(calcPercent(ev.clientX, ev.clientY));
84123
const onUp = () => {
85124
document.removeEventListener("mousemove", onMove);
86125
document.removeEventListener("mouseup", onUp);
@@ -91,42 +130,64 @@ export const HorizontalThresholdSlider: React.FC<HorizontalThresholdSliderProps>
91130

92131
const isEnabled = config.threshold < DISABLE_THRESHOLD;
93132
const color = isEnabled ? "var(--color-plan-mode)" : "var(--color-muted)";
94-
const title = isEnabled
95-
? `Auto-compact at ${config.threshold}% · Drag to adjust (per-model)`
96-
: "Auto-compact disabled · Drag left to enable (per-model)";
133+
const title = getTooltip(config.threshold, orientation);
134+
135+
// Container styles
136+
const containerStyle: React.CSSProperties = {
137+
position: "absolute",
138+
cursor: isHorizontal ? "ew-resize" : "ns-resize",
139+
top: 0,
140+
bottom: 0,
141+
left: 0,
142+
right: 0,
143+
zIndex: 50,
144+
};
97145

98-
return (
99-
<div
100-
ref={containerRef}
101-
style={{
102-
position: "absolute",
103-
cursor: "ew-resize",
104-
top: 0,
105-
bottom: 0,
106-
left: 0,
107-
right: 0,
108-
zIndex: 50,
109-
}}
110-
onMouseDown={handleMouseDown}
111-
title={title}
112-
>
113-
{/* Indicator: top triangle + line + bottom triangle, centered on threshold */}
114-
<div
115-
style={{
116-
position: "absolute",
146+
// Indicator positioning - use transform for centering on both axes
147+
const indicatorStyle: React.CSSProperties = {
148+
position: "absolute",
149+
pointerEvents: "none",
150+
display: "flex",
151+
alignItems: "center",
152+
...(isHorizontal
153+
? {
117154
left: `${config.threshold}%`,
118-
top: `calc(50% - ${TRIANGLE_SIZE + 3}px)`,
119-
transform: "translateX(-50%)",
120-
pointerEvents: "none",
121-
display: "flex",
155+
top: "50%",
156+
transform: "translate(-50%, -50%)",
122157
flexDirection: "column",
123-
alignItems: "center",
124-
}}
125-
>
126-
<Triangle direction="down" color={color} />
127-
<div style={{ width: 1, height: 6, background: color }} />
128-
<Triangle direction="up" color={color} />
158+
}
159+
: {
160+
top: `${config.threshold}%`,
161+
left: "50%",
162+
transform: "translate(-50%, -50%)",
163+
flexDirection: "row",
164+
}),
165+
};
166+
167+
// Line between triangles
168+
const lineStyle: React.CSSProperties = isHorizontal
169+
? { width: 1, height: 6, background: color }
170+
: { width: 6, height: 1, background: color };
171+
172+
return (
173+
<div ref={containerRef} style={containerStyle} onMouseDown={handleMouseDown} title={title}>
174+
<div style={indicatorStyle}>
175+
<Triangle direction={isHorizontal ? "down" : "right"} color={color} />
176+
<div style={lineStyle} />
177+
<Triangle direction={isHorizontal ? "up" : "left"} color={color} />
129178
</div>
130179
</div>
131180
);
132181
};
182+
183+
// ----- Convenience exports -----
184+
185+
/** Horizontal threshold slider (alias for backwards compatibility) */
186+
export const HorizontalThresholdSlider: React.FC<{ config: AutoCompactionConfig }> = ({
187+
config,
188+
}) => <ThresholdSlider config={config} orientation="horizontal" />;
189+
190+
/** Vertical threshold slider */
191+
export const VerticalThresholdSlider: React.FC<{ config: AutoCompactionConfig }> = ({ config }) => (
192+
<ThresholdSlider config={config} orientation="vertical" />
193+
);

‎src/browser/components/RightSidebar/VerticalTokenMeter.tsx‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import { TooltipWrapper, Tooltip } from "../Tooltip";
33
import { TokenMeter } from "./TokenMeter";
4-
import type { AutoCompactionConfig } from "./ThresholdSlider";
4+
import { VerticalThresholdSlider, type AutoCompactionConfig } from "./ThresholdSlider";
55
import {
66
type TokenMeterData,
77
formatTokens,
@@ -10,11 +10,14 @@ import {
1010

1111
interface VerticalTokenMeterProps {
1212
data: TokenMeterData;
13-
/** Auto-compaction settings - reserved for future vertical slider */
13+
/** Auto-compaction settings for threshold slider */
1414
autoCompaction?: AutoCompactionConfig;
1515
}
1616

17-
const VerticalTokenMeterComponent: React.FC<VerticalTokenMeterProps> = ({ data }) => {
17+
const VerticalTokenMeterComponent: React.FC<VerticalTokenMeterProps> = ({
18+
data,
19+
autoCompaction,
20+
}) => {
1821
if (data.segments.length === 0) return null;
1922

2023
// Scale the bar based on context window usage (0-100%)
@@ -35,14 +38,15 @@ const VerticalTokenMeterComponent: React.FC<VerticalTokenMeterProps> = ({ data }
3538
</div>
3639
)}
3740

38-
{/* Bar container - flex to scale bar proportionally to usage */}
39-
<div className="flex min-h-0 w-full flex-1 flex-col items-center">
41+
{/* Bar container - relative for slider positioning, flex for proportional scaling */}
42+
<div className="relative flex min-h-0 w-full flex-1 flex-col items-center">
4043
{/* Used portion - grows based on usage percentage */}
4144
<div
42-
className="flex min-h-[20px] w-full flex-col items-center px-[6px]"
45+
className="flex min-h-[20px] w-full flex-col items-center"
4346
style={{ flex: usagePercentage }}
4447
>
45-
<div className="flex flex-1 flex-col">
48+
{/* [&>*] selector makes TooltipWrapper fill available space */}
49+
<div className="flex w-full flex-1 flex-col items-center [&>*]:flex [&>*]:flex-1 [&>*]:flex-col">
4650
<TooltipWrapper>
4751
<TokenMeter
4852
segments={data.segments}
@@ -84,6 +88,9 @@ const VerticalTokenMeterComponent: React.FC<VerticalTokenMeterProps> = ({ data }
8488
</div>
8589
{/* Empty portion - takes remaining space */}
8690
<div className="w-full" style={{ flex: Math.max(0, 100 - usagePercentage) }} />
91+
92+
{/* Threshold slider overlay - only when autoCompaction config provided and maxTokens known */}
93+
{autoCompaction && data.maxTokens && <VerticalThresholdSlider config={autoCompaction} />}
8794
</div>
8895
</div>
8996
);

‎src/common/constants/ui.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@ export const COMPACTED_EMOJI = "📦";
1212

1313
/**
1414
* Auto-compaction threshold bounds (percentage)
15-
* Too low risks frequent interruptions; too high risks hitting context limits
15+
* MIN: Allow any value - user can choose aggressive compaction if desired
16+
* MAX: Cap at 90% to leave buffer before hitting context limit
1617
*/
17-
export const AUTO_COMPACTION_THRESHOLD_MIN = 50;
18+
export const AUTO_COMPACTION_THRESHOLD_MIN = 0;
1819
export const AUTO_COMPACTION_THRESHOLD_MAX = 90;
1920

2021
/**

0 commit comments

Comments
 (0)