Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3dc9cfa
🤖 Add VimTextArea and integrate Vim keybindings into ChatInput (MVP)
ammario Oct 7, 2025
771e74c
Vim: add basic change (c) operator with ciw, cw, cc, C/c$ and mode in…
ammario Oct 7, 2025
e9c839c
refactor: extract Vim logic into utils/vim.ts with comprehensive unit…
ammario Oct 7, 2025
6ef7afa
docs: add comprehensive Vim mode implementation summary
ammario Oct 7, 2025
87fe82c
fix: resolve lint issues and enforce documentation organization
ammario Oct 7, 2025
79c79c0
docs: clarify developer documentation placement
ammario Oct 7, 2025
2e7679d
🤖 fix: improve Vim mode UX - blinking cursor, tiny mode indicator abo…
ammar-agent Oct 8, 2025
32dea96
🤖 fix: add support for uppercase W and B Vim motions
ammar-agent Oct 8, 2025
7b7624a
🤖 fix: improve normal mode cursor visibility and spacing
ammar-agent Oct 8, 2025
817d08f
🤖 fix: clamp cursor to last character in normal mode for w/b motions
ammar-agent Oct 8, 2025
332c827
🤖 feat: use Ctrl+Q to cancel message editing, keep ESC for Vim mode
ammar-agent Oct 8, 2025
cb8bbbf
🤖 fix: remove ESC stream interruption, delegate to Ctrl+C
ammar-agent Oct 8, 2025
b0f98f8
🤖 feat: add composable operator-motion system with d$ and full motion…
ammar-agent Oct 8, 2025
29a63ce
🤖 feat: solid block cursor in normal mode, visible even on empty text
ammar-agent Oct 9, 2025
316051f
🤖 fix: $ motion now goes to last character, not past it
ammar-agent Oct 9, 2025
61a684b
🤖 fix: cursor position when entering normal mode from insert
ammar-agent Oct 9, 2025
3489e91
🤖 test: rewrite Vim tests as integration tests for complete commands
ammar-agent Oct 9, 2025
a0d683b
🤖 docs: add Vim test rewrite summary
ammar-agent Oct 9, 2025
6f14d0a
🤖 fix: text object handling in test harness (ciw, diw, yiw)
ammar-agent Oct 9, 2025
e263935
🤖 fix: ciw leaving blank character highlighted in insert mode
ammar-agent Oct 9, 2025
b8b49fc
🤖 feat: show pending operator in mode indicator and fix layout issues
ammar-agent Oct 9, 2025
c197074
🤖 feat: reduce padding and improve element debugging
ammar-agent Oct 9, 2025
7efd3ac
🤖 feat: add help indicator and vim docs, fix uppercase issue
ammar-agent Oct 9, 2025
f965e0c
🤖 docs: add note about reading docs/README.md before writing user docs
ammar-agent Oct 9, 2025
2f56ca0
🤖 fix: correct mdbook path and add vim-mode to SUMMARY
ammar-agent Oct 9, 2025
6e07563
🤖 feat: reduce top padding and add debugging labels to ChatInput
ammar-agent Oct 9, 2025
bc1a0b0
user edits
ammario Oct 9, 2025
a8f8d56
🤖 Add bidirectional sync comments and remove architecture notes from …
ammar-agent Oct 9, 2025
4d2da44
🤖 Add writing guidelines and remove trivial details from docs
ammar-agent Oct 9, 2025
e3115f0
rm old file
ammario Oct 9, 2025
8b6fd39
🤖 Centralize Vim logic in vim.ts with handleKeyPress() state machine
ammar-agent Oct 9, 2025
7a02f12
🤖 Refactor VimTextArea to use centralized handleKeyPress() - 58% smaller
ammar-agent Oct 9, 2025
cba8a77
🤖 Add 'e' motion (move to end of word)
ammar-agent Oct 9, 2025
07824ac
🤖 Refactor: Eliminate duplication in operator-motion logic
ammar-agent Oct 9, 2025
10a94e2
🤖 Refactor: Improve type safety and reduce state update boilerplate
ammar-agent Oct 9, 2025
52ae594
🤖 Refactor: Extract handleKey helper to eliminate VimKeyResult boiler…
ammar-agent Oct 9, 2025
9c64d9e
🤖 UI: Reduce help indicator size in Vim mode
ammar-agent Oct 9, 2025
818b741
🤖 Fix: Prevent empty space highlight after ciw on non-last words
ammar-agent Oct 9, 2025
fb39a82
🤖 Fix: Resolve ESLint errors in main.ts and vim.ts
ammar-agent Oct 9, 2025
ecc0709
Merge branch 'main' into josh
ammario Oct 9, 2025
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
14 changes: 13 additions & 1 deletion docs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,19 @@ Verify with React DevTools Profiler - MarkdownCore should only re-render when co
- `~/.cmux/src/<project_name>/<branch>` - Workspace directories for git worktrees
- `~/.cmux/sessions/<workspace_id>/chat.jsonl` - Session chat histories

## Docs
## Documentation Guidelines

**Free-floating markdown docs are not permitted.** Documentation must be organized:

- **User-facing docs** → `./docs/` directory
- **IMPORTANT**: Read `docs/README.md` first before writing user-facing documentation
- User docs are built with mdbook and deployed to https://cmux.io
- Must be added to `docs/SUMMARY.md` to appear in the docs
- Use standard markdown + mermaid diagrams
- **Developer docs** → inline with the code its documenting as comments. Consider them notes as notes to future Assistants to understand the logic more quickly.
**DO NOT** create standalone documentation files in the project root or random locations.

### External API Docs

DO NOT visit https://sdk.vercel.ai/docs/ai-sdk-core. All of that content is already
in `./ai-sdk-docs/**.mdx`.
Expand Down
25 changes: 25 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,31 @@ docs/
2. Add it to `src/SUMMARY.md` to make it appear in the sidebar
3. Use standard markdown + mermaid diagrams

## Writing Guidelines

**Focus on what matters. Avoid documenting trivia.**

- **Don't document expected behavior** - If your target audience already expects it, don't state it
- **Don't document obvious details** - Implementation details that "just work" don't need explanation
- **Document what's different** - Deviations from expectations, gotchas, design decisions
- **Document what's complex** - Multi-step workflows, non-obvious interactions, tradeoffs

### Examples of What NOT to Document

❌ "The cursor is always visible, even on empty text" - Expected Vim behavior, trivial detail

❌ "The save button is in the top right" - Obvious from UI, no cognitive value

❌ "Press Enter to submit" - Universal convention, doesn't need stating

### Examples of What TO Document

✅ "ESC exits normal mode instead of canceling edits (use Ctrl-Q)" - Different from expected behavior

✅ "Column position is preserved when moving up/down" - Non-obvious Vim feature some users don't know

✅ "Operators compose with motions: d + w = dw" - Core concept that unlocks understanding

### Example Mermaid Diagram

````markdown
Expand Down
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
- [Introduction](./intro.md)
- [Install](./install.md)
- [Keyboard Shortcuts](./keybinds.md)
- [Vim Mode](./vim-mode.md)
- [Context Management](./context-management.md)
- [Project Secrets](./project-secrets.md)
- [Agentic Git Identity](./agentic-git-identity.md)
Expand Down
1 change: 1 addition & 0 deletions docs/keybinds.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ When documentation shows `Ctrl`, it means:
| Focus chat input | `a` or `i` |
| Send message | `Enter` |
| New line in message | `Shift+Enter` |
| Cancel editing message | `Ctrl+Q` |
| Jump to bottom of chat | `Shift+G` |
| Change model | `Ctrl+/` |

Expand Down
152 changes: 152 additions & 0 deletions docs/vim-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<!-- Keep this file in sync with:
- src/components/VimTextArea.tsx (implementation)
- src/utils/vim.ts (core logic)
- src/utils/vim.test.ts (test suite)
-->

# Vim Mode

cmux includes a built-in Vim mode for the chat input, providing familiar Vim-style editing for power users.

## Enabling Vim Mode

Vim mode is always enabled. Press **ESC** to enter normal mode from insert mode.

## Modes

### Insert Mode (Default)
- This is the default mode when typing in the chat input
- Type normally, all characters are inserted
- Press **ESC** or **Ctrl-[** to enter normal mode

### Normal Mode
- Command mode for navigation and editing
- Indicated by "NORMAL" text above the input
- Pending commands are shown (e.g., "NORMAL d" when delete is pending)
- Press **i**, **a**, **I**, **A**, **o**, or **O** to return to insert mode

## Navigation

### Basic Movement
- **h** - Move left one character
- **j** - Move down one line
- **k** - Move up one line
- **l** - Move right one character

### Word Movement
- **w** - Move forward to start of next word
- **W** - Move forward to start of next WORD (whitespace-separated)
- **b** - Move backward to start of previous word
- **B** - Move backward to start of previous WORD
- **e** - Move to end of current/next word
- **E** - Move to end of current/next WORD

### Line Movement
- **0** - Move to beginning of line
- **$** - Move to end of line
- **Home** - Same as **0**
- **End** - Same as **$**

### Column Preservation
When moving up/down with **j**/**k**, the cursor attempts to stay in the same column position. If a line is shorter, the cursor moves to the end of that line, but will return to the original column on longer lines.

## Entering Insert Mode

- **i** - Insert at cursor
- **a** - Append after cursor
- **I** - Insert at beginning of line
- **A** - Append at end of line
- **o** - Open new line below and insert
- **O** - Open new line above and insert

## Editing Commands

### Simple Edits
- **x** - Delete character under cursor
- **p** - Paste after cursor
- **P** - Paste before cursor

### Undo/Redo
- **u** - Undo last change
- **Ctrl-r** - Redo

### Line Operations
- **dd** - Delete line (yank to clipboard)
- **yy** - Yank (copy) line
- **cc** - Change line (delete and enter insert mode)

## Operators + Motions

Vim's power comes from combining operators with motions. All operators work with all motions:

### Operators
- **d** - Delete
- **c** - Change (delete and enter insert mode)
- **y** - Yank (copy)

### Motions
- **w** - To next word
- **b** - To previous word
- **e** - To end of word
- **$** - To end of line
- **0** - To beginning of line

### Examples
- **dw** - Delete to next word
- **de** - Delete to end of word
- **d$** - Delete to end of line
- **cw** - Change to next word
- **ce** - Change to end of word
- **c0** - Change to beginning of line
- **y$** - Yank to end of line
- **ye** - Yank to end of word
- **yy** - Yank line (doubled operator)

### Shortcuts
- **D** - Same as **d$** (delete to end of line)
- **C** - Same as **c$** (change to end of line)

## Text Objects

Text objects let you operate on semantic units:

### Inner Word (iw)
- **diw** - Delete inner word (word under cursor)
- **ciw** - Change inner word
- **yiw** - Yank inner word

Text objects work from anywhere within the word - you don't need to be at the start.

## Visual Feedback

- **Cursor**: Thin blinking cursor in insert mode, solid block in normal mode
- **Mode Indicator**: Shows current mode and pending commands (e.g., "NORMAL d" when waiting for motion)

## Keybind Conflicts

### ESC Key
ESC is used for:
1. Exiting Vim normal mode (highest priority)
2. NOT used for canceling edits (use **Ctrl-Q** instead)
3. NOT used for interrupting streams (use **Ctrl-C** instead)



## Tips

1. **Learn operators + motions**: Instead of memorizing every command, learn the operators (d, c, y) and motions (w, b, $, 0). They combine naturally.

2. **Use text objects**: `ciw` to change a word is more reliable than `cw` because it works from anywhere in the word.

3. **Column preservation**: When navigating up/down, your column position is preserved across lines of different lengths.

## Not Yet Implemented

Features that may be added in the future:
- **ge** - Backward end of word motion
- **f{char}**, **t{char}** - Find character motions
- **i"**, **i'**, **i(**, **i[**, **i{** - More text objects
- **2w**, **3dd**, **5x** - Count prefixes
- **Visual mode** - Character, line, and block selection
- **Macros** - Recording and replaying command sequences
- **Marks** - Named cursor positions
88 changes: 28 additions & 60 deletions src/components/ChatInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ import { matchesKeybind, formatKeybind, KEYBINDS, isEditableElement } from "@/ut
import { defaultModel } from "@/utils/ai/models";
import { ModelSelector, type ModelSelectorRef } from "./ModelSelector";
import { useModelLRU } from "@/hooks/useModelLRU";
import { VimTextArea } from "./VimTextArea";

const InputSection = styled.div`
position: relative;
padding: 15px;
padding: 5px 15px 15px 15px; /* Reduced top padding from 15px to 5px */
background: #252526;
border-top: 1px solid #3e3e42;
display: flex;
Expand All @@ -40,39 +41,7 @@ const InputControls = styled.div`
align-items: flex-end;
`;

const InputField = styled.textarea<{
isEditing?: boolean;
canInterrupt?: boolean;
mode: UIMode;
}>`
flex: 1;
background: ${(props) => (props.isEditing ? "var(--color-editing-mode-alpha)" : "#1e1e1e")};
border: 1px solid ${(props) => (props.isEditing ? "var(--color-editing-mode)" : "#3e3e42")};
color: #d4d4d4;
padding: 8px 12px;
border-radius: 4px;
font-family: inherit;
font-size: 13px;
resize: none;
min-height: 36px;
max-height: 200px;
overflow-y: auto;
max-height: 120px;

&:focus {
outline: none;
border-color: ${(props) =>
props.isEditing
? "var(--color-editing-mode)"
: props.mode === "plan"
? "var(--color-plan-mode)"
: "var(--color-exec-mode)"};
}

&::placeholder {
color: #6b6b6b;
}
`;
// Input now rendered by VimTextArea; styles moved there

const ModeToggles = styled.div`
display: flex;
Expand Down Expand Up @@ -670,21 +639,24 @@ export const ChatInput: React.FC<ChatInputProps> = ({
return;
}

// Handle cancel/escape
if (matchesKeybind(e, KEYBINDS.CANCEL)) {
const isFocused = document.activeElement === inputRef.current;
e.preventDefault();

// Cancel editing if in edit mode
// Handle cancel edit (Ctrl+Q)
if (matchesKeybind(e, KEYBINDS.CANCEL_EDIT)) {
if (editingMessage && onCancelEdit) {
e.preventDefault();
onCancelEdit();
const isFocused = document.activeElement === inputRef.current;
if (isFocused) {
inputRef.current?.blur();
}
return;
}
}

if (isFocused) {
inputRef.current?.blur();
}

return;
// Handle escape - let VimTextArea handle it (for Vim mode transitions)
// Edit canceling is handled by Ctrl+Q above
// Stream interruption is handled by Ctrl+C (INTERRUPT_STREAM keybind)
if (matchesKeybind(e, KEYBINDS.CANCEL)) {
// Do not preventDefault here: allow VimTextArea or other handlers (like suggestions) to process ESC
}

// Don't handle keys if command suggestions are visible
Expand Down Expand Up @@ -730,37 +702,33 @@ export const ChatInput: React.FC<ChatInputProps> = ({
})();

return (
<InputSection>
<InputSection data-component="ChatInputSection">
<ChatInputToast toast={toast} onDismiss={handleToastDismiss} />
<CommandSuggestions
suggestions={commandSuggestions}
onSelectSuggestion={handleCommandSelect}
onDismiss={() => setShowCommandSuggestions(false)}
isVisible={showCommandSuggestions}
/>
<InputControls>
<InputField
<InputControls data-component="ChatInputControls">
<VimTextArea
ref={inputRef}
value={input}
isEditing={!!editingMessage}
mode={mode}
onChange={(e) => {
const newValue = e.target.value;
setInput(newValue);
// Auto-resize textarea
e.target.style.height = "auto";
e.target.style.height = Math.min(e.target.scrollHeight, 200) + "px";

// Don't clear toast when typing - let user dismiss it manually or it auto-dismisses
}}
onChange={setInput}
onKeyDown={handleKeyDown}
suppressKeys={showCommandSuggestions ? COMMAND_SUGGESTION_KEYS : undefined}
placeholder={placeholder}
disabled={disabled || isSending || isCompacting}
canInterrupt={canInterrupt}
/>
</InputControls>
<ModeToggles>
{editingMessage && <EditingIndicator>Editing message (ESC to cancel)</EditingIndicator>}
<ModeToggles data-component="ChatModeToggles">
{editingMessage && (
<EditingIndicator>
Editing message ({formatKeybind(KEYBINDS.CANCEL_EDIT)} to cancel)
</EditingIndicator>
)}
<ModeTogglesRow>
<ChatToggles workspaceId={workspaceId} modelString={preferredModel}>
<ModelDisplayWrapper>
Expand Down
8 changes: 4 additions & 4 deletions src/components/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -186,15 +186,15 @@ const Arrow = styled.div`

export const HelpIndicator = styled.span`
color: #666666;
font-size: 8px;
font-size: 7px;
cursor: help;
display: inline-block;
vertical-align: baseline;
border: 1px solid #666666;
border-radius: 50%;
width: 11px;
height: 11px;
line-height: 9px;
width: 10px;
height: 10px;
line-height: 8px;
text-align: center;
font-weight: bold;
margin-bottom: 2px;
Expand Down
Loading
Loading