Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions docs/keybinds.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ description: Complete keyboard shortcut reference for mux
mux is designed to be keyboard-driven for maximum efficiency. All major actions have keyboard shortcuts.

<Info>
This document should be kept in sync with `src/utils/ui/keybinds.ts`, which is the source of truth
for keybind definitions.
This document should be kept in sync with `src/browser/utils/ui/keybinds.ts`, which is the source
of truth for keybind definitions.
</Info>

## Platform Conventions
Expand All @@ -33,9 +33,9 @@ When documentation shows `Ctrl`, it means:
| Focus chat input | `a`, `i`, or `Ctrl+I` |
| Send message | `Enter` |
| New line in message | `Shift+Enter` |
| Cancel editing message | `Ctrl+Q` |
| Cancel editing message | `Esc` |
| Jump to bottom of chat | `Shift+G` |
| Change model | `Ctrl+/` |
| Cycle model | `Ctrl+/` |
| Toggle thinking level | `Ctrl+Shift+T` |

## Workspaces
Expand Down
9 changes: 6 additions & 3 deletions docs/models.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -363,10 +363,13 @@ All providers are configured in `~/.mux/providers.jsonc`. Example configurations

### Model Selection

The quickest way to switch models is with the keyboard shortcut:
Keyboard shortcuts:

- **macOS:** `Cmd+/`
- **Windows/Linux:** `Ctrl+/`
- **Cycle models**
- **macOS:** `Cmd+/`
- **Windows/Linux:** `Ctrl+/`

To _choose_ a specific model, click the model pill in the chat footer.

Alternatively, use the Command Palette (`Cmd+Shift+P` / `Ctrl+Shift+P`):

Expand Down
28 changes: 0 additions & 28 deletions src/browser/components/AIView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import type { DisplayedMessage } from "@/common/types/message";
import type { RuntimeConfig } from "@/common/types/runtime";
import { getRuntimeTypeForTelemetry } from "@/common/telemetry";
import { useAIViewKeybinds } from "@/browser/hooks/useAIViewKeybinds";
import { evictModelFromLRU } from "@/browser/hooks/useModelLRU";
import { QueuedMessage } from "./Messages/QueuedMessage";
import { CompactionWarning } from "./CompactionWarning";
import { ConcurrentLocalWarning } from "./ConcurrentLocalWarning";
Expand Down Expand Up @@ -145,33 +144,6 @@ const AIViewInner: React.FC<AIViewProps> = ({
workspaceId,
pendingModel
);
const handledModelErrorsRef = useRef<Set<string>>(new Set());

useEffect(() => {
handledModelErrorsRef.current.clear();
}, [workspaceId]);

useEffect(() => {
if (!workspaceState) {
return;
}

for (const message of workspaceState.messages) {
if (message.type !== "stream-error") {
continue;
}
if (message.errorType !== "model_not_found") {
continue;
}
if (handledModelErrorsRef.current.has(message.id)) {
continue;
}
handledModelErrorsRef.current.add(message.id);
if (message.model) {
evictModelFromLRU(message.model);
}
}
}, [workspaceState, workspaceId]);

const [editingMessage, setEditingMessage] = useState<{ id: string; content: string } | undefined>(
undefined
Expand Down
77 changes: 60 additions & 17 deletions src/browser/components/ChatInput/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {
isEditableElement,
} from "@/browser/utils/ui/keybinds";
import { ModelSelector, type ModelSelectorRef } from "../ModelSelector";
import { useModelLRU } from "@/browser/hooks/useModelLRU";
import { useModelsFromSettings } from "@/browser/hooks/useModelsFromSettings";
import { SendHorizontal, X } from "lucide-react";
import { VimTextArea } from "../VimTextArea";
import { ImageAttachments, type ImageAttachment } from "../ImageAttachments";
Expand Down Expand Up @@ -180,7 +180,16 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
const { open } = useSettings();
const { selectedWorkspace } = useWorkspaceContext();
const [mode, setMode] = useMode();
const { recentModels, addModel, defaultModel, setDefaultModel } = useModelLRU();
const {
models,
customModels,
hiddenModels,
hideModel,
unhideModel,
ensureModelInSettings,
defaultModel,
setDefaultModel,
} = useModelsFromSettings();
const commandListId = useId();
const telemetry = useTelemetry();
const [vimEnabled, setVimEnabled] = usePersistedState<boolean>(VIM_ENABLED_KEY, false, {
Expand Down Expand Up @@ -229,6 +238,14 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
// - baseModel: canonical format for UI display and policy checks (e.g., ThinkingSlider)
const preferredModel = sendMessageOptions.model;
const baseModel = sendMessageOptions.baseModel;

const setPreferredModel = useCallback(
(model: string) => {
ensureModelInSettings(model); // Ensure model exists in Settings
updatePersistedState(storageKeys.modelKey, model); // Update workspace or project-specific
},
[storageKeys.modelKey, ensureModelInSettings]
);
const deferredModel = useDeferredValue(preferredModel);
const deferredInput = useDeferredValue(input);
const tokenCountPromise = useMemo(() => {
Expand All @@ -243,15 +260,29 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
[tokenCountPromise]
);

// Setter for model - updates localStorage directly so useSendMessageOptions picks it up
const setPreferredModel = useCallback(
(model: string) => {
addModel(model); // Update LRU
updatePersistedState(storageKeys.modelKey, model); // Update workspace or project-specific
},
[storageKeys.modelKey, addModel]
// Model cycling candidates. Prefer the user's custom model list (as configured in Settings).
// If no custom models are configured, fall back to the full suggested list.
const cycleModels = useMemo(
() => (customModels.length > 0 ? customModels : models),
[customModels, models]
);

const cycleToNextModel = useCallback(() => {
if (cycleModels.length < 2) {
return;
}

const currentIndex = cycleModels.indexOf(baseModel);
const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % cycleModels.length;
const nextModel = cycleModels[nextIndex];
if (nextModel) {
setPreferredModel(nextModel);
}
}, [baseModel, cycleModels, setPreferredModel]);

const openModelSelector = useCallback(() => {
modelSelectorRef.current?.open();
}, []);
// Creation-specific state (hook always called, but only used when variant === "creation")
// This avoids conditional hook calls which violate React rules
const creationState = useCreationWorkspace(
Expand Down Expand Up @@ -388,14 +419,21 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
if (matchesKeybind(event, KEYBINDS.FOCUS_INPUT_A)) {
event.preventDefault();
focusMessageInput();
return;
}

if (matchesKeybind(event, KEYBINDS.CYCLE_MODEL)) {
event.preventDefault();
focusMessageInput();
cycleToNextModel();
}
};

window.addEventListener("keydown", handleGlobalKeyDown);
return () => {
window.removeEventListener("keydown", handleGlobalKeyDown);
};
}, [focusMessageInput]);
}, [cycleToNextModel, focusMessageInput, openModelSelector]);

// When entering editing mode, save current draft and populate with message content
useEffect(() => {
Expand Down Expand Up @@ -1221,10 +1259,10 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
return;
}

// Handle open model selector
if (matchesKeybind(e, KEYBINDS.OPEN_MODEL_SELECTOR)) {
// Cycle models (Ctrl+/)
if (matchesKeybind(e, KEYBINDS.CYCLE_MODEL)) {
e.preventDefault();
modelSelectorRef.current?.open();
cycleToNextModel();
return;
}

Expand Down Expand Up @@ -1306,7 +1344,7 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
hints.push(`${formatKeybind(interruptKeybind)} to interrupt`);
}
hints.push(`${formatKeybind(KEYBINDS.SEND_MESSAGE)} to ${canInterrupt ? "queue" : "send"}`);
hints.push(`${formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)} to change model`);
hints.push(`Click model to choose, ${formatKeybind(KEYBINDS.CYCLE_MODEL)} to cycle`);
hints.push(`/vim to toggle Vim mode (${vimEnabled ? "on" : "off"})`);

return `Type a message... (${hints.join(", ")})`;
Expand Down Expand Up @@ -1500,18 +1538,23 @@ const ChatInputInner: React.FC<ChatInputProps> = (props) => {
ref={modelSelectorRef}
value={baseModel}
onChange={setPreferredModel}
recentModels={recentModels}
models={models}
onComplete={() => inputRef.current?.focus()}
defaultModel={defaultModel}
onSetDefaultModel={setDefaultModel}
onHideModel={hideModel}
hiddenModels={hiddenModels}
onUnhideModel={unhideModel}
onOpenSettings={() => open("models")}
/>
<Tooltip>
<TooltipTrigger asChild>
<HelpIndicator>?</HelpIndicator>
</TooltipTrigger>
<TooltipContent align="start" className="max-w-80 whitespace-normal">
<strong>Click to edit</strong> or use{" "}
{formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)}
<strong>Click to edit</strong>
<br />
<strong>{formatKeybind(KEYBINDS.CYCLE_MODEL)}</strong> to cycle models
<br />
<br />
<strong>Abbreviations:</strong>
Expand Down
Loading