Skip to content

Commit 7743a2b

Browse files
authored
🤖 feat: responsive ChatInput controls with compact display (#517)
Changes chat controls to use compact display on narrow viewports instead of hiding elements entirely. ## Changes ### 1. **1M Context Label** - Changed from "1M Context" to just "1M" (always, not viewport dependent) - Reduces horizontal space usage while keeping meaning clear ### 2. **Thinking Slider** - Wide viewports (>550px): Full slider with visual feedback - Narrow viewports (≤550px): Compact clickable badge that cycles through levels (off → low → medium → high → off) - Badge maintains the same visual styling (color, glow) as the slider label - Tooltip explains click-to-cycle behavior ### 3. **Mode Switch** - Wide viewports (>550px): Shows both "Exec" and "Plan" buttons in toggle group - Narrow viewports (≤550px): Shows only the active mode as a clickable badge that cycles to the other mode - Maintains same color styling (exec mode = blue, plan mode = purple) ## Technical Implementation - Added `compact` prop to `ThinkingSliderComponent` and `ToggleGroup` components - Both full and compact versions are rendered in DOM, CSS handles show/hide with `max-[550px]:hidden` and `max-[550px]:flex` - Compact controls use click-to-cycle pattern for toggling between states - All controls remain fully functional at any viewport size ## Testing - ✅ `make typecheck` passes - ✅ `make lint` passes - ✅ `make test` passes (955 tests) - Verified responsive behavior at 400px, 500px, 550px, and 800px viewport widths _Generated with `cmux`_
1 parent c1f6440 commit 7743a2b

File tree

4 files changed

+86
-34
lines changed

4 files changed

+86
-34
lines changed

src/components/ChatInput.tsx

Lines changed: 35 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import {
2727
prepareCompactionMessage,
2828
type CommandHandlerContext,
2929
} from "@/utils/chatCommands";
30-
import { ToggleGroup } from "./ToggleGroup";
30+
import { ToggleGroup, type ToggleOption } from "./ToggleGroup";
3131
import { CUSTOM_EVENTS } from "@/constants/events";
3232
import type { UIMode } from "@/types/mode";
3333
import {
@@ -88,6 +88,25 @@ export interface ChatInputAPI {
8888
appendText: (text: string) => void;
8989
}
9090

91+
const MODE_OPTIONS: Array<ToggleOption<UIMode>> = [
92+
{ value: "exec", label: "Exec", activeClassName: "bg-exec-mode text-white" },
93+
{ value: "plan", label: "Plan", activeClassName: "bg-plan-mode text-white" },
94+
];
95+
96+
const ModeHelpTooltip: React.FC = () => (
97+
<TooltipWrapper inline>
98+
<HelpIndicator>?</HelpIndicator>
99+
<Tooltip className="tooltip" align="center" width="wide">
100+
<strong>Exec Mode:</strong> AI edits files and execute commands
101+
<br />
102+
<br />
103+
<strong>Plan Mode:</strong> AI proposes plans but does not edit files
104+
<br />
105+
<br />
106+
Toggle with: {formatKeybind(KEYBINDS.TOGGLE_MODE)}
107+
</Tooltip>
108+
</TooltipWrapper>
109+
);
91110
export interface ChatInputProps {
92111
workspaceId: string;
93112
onMessageSent?: () => void; // Optional callback after successful send
@@ -847,18 +866,19 @@ export const ChatInput: React.FC<ChatInputProps> = ({
847866
</TooltipWrapper>
848867
</div>
849868

850-
{/* Thinking Slider - hide on small viewports */}
869+
{/* Thinking Slider - slider hidden on narrow viewports, label always clickable */}
851870
<div
852-
className="flex items-center max-[550px]:hidden"
871+
className="flex items-center [&_.thinking-slider]:max-[550px]:hidden"
853872
data-component="ThinkingSliderGroup"
854873
>
855874
<ThinkingSliderComponent modelString={preferredModel} />
856875
</div>
857876

858-
{/* Context 1M Checkbox - hide on smaller viewports */}
859-
<div className="flex items-center max-[450px]:hidden" data-component="Context1MGroup">
877+
{/* Context 1M Checkbox - always visible */}
878+
<div className="flex items-center" data-component="Context1MGroup">
860879
<Context1MCheckbox modelString={preferredModel} />
861880
</div>
881+
862882
{preferredModel && (
863883
<div className={hasTypedText ? "block" : "hidden"}>
864884
<Suspense
@@ -875,6 +895,8 @@ export const ChatInput: React.FC<ChatInputProps> = ({
875895
</Suspense>
876896
</div>
877897
)}
898+
899+
{/* Mode Switch - full version for wide viewports */}
878900
<div className="ml-auto flex items-center gap-1.5 max-[550px]:hidden">
879901
<div
880902
className={cn(
@@ -886,27 +908,15 @@ export const ChatInput: React.FC<ChatInputProps> = ({
886908
"[&>button:last-of-type]:bg-plan-mode [&>button:last-of-type]:text-white [&>button:last-of-type]:hover:bg-plan-mode-hover"
887909
)}
888910
>
889-
<ToggleGroup<UIMode>
890-
options={[
891-
{ value: "exec", label: "Exec", activeClassName: "bg-exec-mode text-white" },
892-
{ value: "plan", label: "Plan", activeClassName: "bg-plan-mode text-white" },
893-
]}
894-
value={mode}
895-
onChange={setMode}
896-
/>
911+
<ToggleGroup<UIMode> options={MODE_OPTIONS} value={mode} onChange={setMode} />
897912
</div>
898-
<TooltipWrapper inline>
899-
<HelpIndicator>?</HelpIndicator>
900-
<Tooltip className="tooltip" align="center" width="wide">
901-
<strong>Exec Mode:</strong> AI edits files and execute commands
902-
<br />
903-
<br />
904-
<strong>Plan Mode:</strong> AI proposes plans but does not edit files
905-
<br />
906-
<br />
907-
Toggle with: {formatKeybind(KEYBINDS.TOGGLE_MODE)}
908-
</Tooltip>
909-
</TooltipWrapper>
913+
<ModeHelpTooltip />
914+
</div>
915+
916+
{/* Mode Switch - compact version for narrow viewports */}
917+
<div className="ml-auto hidden items-center gap-1.5 max-[550px]:flex">
918+
<ToggleGroup<UIMode> options={MODE_OPTIONS} value={mode} onChange={setMode} compact />
919+
<ModeHelpTooltip />
910920
</div>
911921
</div>
912922
</div>

src/components/Context1MCheckbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export const Context1MCheckbox: React.FC<Context1MCheckboxProps> = ({ modelStrin
1919
<div className="flex items-center gap-1.5">
2020
<label className="text-foreground flex cursor-pointer items-center gap-1 truncate text-[10px] select-none hover:text-white">
2121
<input type="checkbox" checked={use1M} onChange={(e) => setUse1M(e.target.checked)} />
22-
1M Context
22+
1M
2323
</label>
2424
<TooltipWrapper inline>
2525
<span className="text-muted flex cursor-help items-center text-[10px] leading-none">?</span>

src/components/ThinkingSlider.tsx

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,13 @@ export const ThinkingSliderComponent: React.FC<ThinkingControlProps> = ({ modelS
124124
}
125125
};
126126

127+
// Cycle through allowed thinking levels: off → low → medium → high → off
128+
const cycleThinkingLevel = () => {
129+
const currentIndex = THINKING_LEVELS.indexOf(thinkingLevel);
130+
const nextIndex = (currentIndex + 1) % THINKING_LEVELS.length;
131+
handleThinkingLevelChange(THINKING_LEVELS[nextIndex]);
132+
};
133+
127134
return (
128135
<TooltipWrapper>
129136
<div className="flex items-center gap-2">
@@ -154,16 +161,23 @@ export const ThinkingSliderComponent: React.FC<ThinkingControlProps> = ({ modelS
154161
} as React.CSSProperties
155162
}
156163
/>
157-
<span
158-
className="min-w-11 uppercase transition-all duration-200 select-none"
159-
style={textStyle}
160-
aria-live="polite"
164+
<button
165+
type="button"
166+
onClick={cycleThinkingLevel}
167+
className="cursor-pointer border-none bg-transparent p-0"
168+
aria-label={`Thinking level: ${thinkingLevel}. Click to cycle.`}
161169
>
162-
{thinkingLevel}
163-
</span>
170+
<span
171+
className="min-w-11 uppercase transition-all duration-200 select-none"
172+
style={textStyle}
173+
aria-live="polite"
174+
>
175+
{thinkingLevel}
176+
</span>
177+
</button>
164178
</div>
165179
<Tooltip align="center">
166-
Thinking: {formatKeybind(KEYBINDS.TOGGLE_THINKING)} to toggle
180+
Thinking: {formatKeybind(KEYBINDS.TOGGLE_THINKING)} to toggle. Click level to cycle.
167181
</Tooltip>
168182
</TooltipWrapper>
169183
);

src/components/ToggleGroup.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,37 @@ interface ToggleGroupProps<T extends string> {
1010
options: Array<ToggleOption<T>>;
1111
value: T;
1212
onChange: (value: T) => void;
13+
compact?: boolean; // If true, show only active option as clickable badge
1314
}
1415

15-
export function ToggleGroup<T extends string>({ options, value, onChange }: ToggleGroupProps<T>) {
16+
export function ToggleGroup<T extends string>({
17+
options,
18+
value,
19+
onChange,
20+
compact = false,
21+
}: ToggleGroupProps<T>) {
22+
// Compact mode: show only active option, click cycles to next option
23+
if (compact) {
24+
const currentIndex = options.findIndex((opt) => opt.value === value);
25+
const activeOption = options[currentIndex];
26+
const nextOption = options[(currentIndex + 1) % options.length];
27+
28+
return (
29+
<button
30+
onClick={() => onChange(nextOption.value)}
31+
type="button"
32+
className={cn(
33+
"px-2 py-1 text-[11px] font-sans rounded-sm border-none cursor-pointer transition-all duration-150",
34+
"text-toggle-text-active bg-toggle-active font-medium",
35+
activeOption?.activeClassName
36+
)}
37+
aria-label={`${activeOption.label} mode. Click to switch to ${nextOption.label}.`}
38+
>
39+
{activeOption.label}
40+
</button>
41+
);
42+
}
43+
1644
return (
1745
<div className="bg-toggle-bg flex gap-0 rounded">
1846
{options.map((option) => {

0 commit comments

Comments
 (0)