Skip to content

feat: Add vi mode for Pair Writing (#394)#433

Merged
rjroy merged 18 commits intomainfrom
feat/394-vi-mode
Jan 30, 2026
Merged

feat: Add vi mode for Pair Writing (#394)#433
rjroy merged 18 commits intomainfrom
feat/394-vi-mode

Conversation

@rjroy
Copy link
Copy Markdown
Owner

@rjroy rjroy commented Jan 30, 2026

Summary

  • Add optional vi-style modal editing to Pair Writing mode, controlled by vault configuration
  • Three modes: Normal (default), Insert, Command
  • Movement commands: h, j, k, l, 0, $
  • Insert mode entry: i, a, A, o, O
  • Edit commands: x, dd, yy, p, P, u (undo)
  • Numeric prefixes: 5j, 3dd, etc.
  • Ex commands: :w, :wq, :q, :q!
  • Keyboard detection automatically disables vi mode on touch-only devices
  • Visual indicators: block cursor overlay, mode indicator, command line

Test plan

  • All 2027 tests pass (273 new tests added)
  • TypeScript type-checks clean
  • ESLint passes
  • Manual testing: enable viMode: true in vault config, verify:
    • Block cursor appears in Normal mode
    • i enters Insert mode (cursor changes to line)
    • Escape returns to Normal mode
    • Movement keys work (hjkl, 0$)
    • Edit commands work (x, dd, yy, p)
    • :w saves, :wq saves and exits, :q! exits without saving
  • Touch device: verify vi mode auto-disabled (mode indicator hidden)

Closes #394

🤖 Generated with Claude Code

rjroy and others added 8 commits January 29, 2026 08:23
Implements foundation for vi-style modal editing in Pair Writing mode:

- Configuration: Add viMode to VaultConfig, flow through to VaultInfo
- Keyboard detection: useHasKeyboard hook to gate vi mode on physical keyboard
- State machine: useViMode hook with normal/insert/command mode transitions
- Cursor overlay: useViCursor hook + ViCursor component with mirror element technique
- Mode indicator: ViModeIndicator component showing current mode

This is the infrastructure layer. Movement commands, editing, and ex commands
come in subsequent chunks.

Refs #394

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Export ViCursor from pair-writing/index.ts for public API
- Remove duplicate ViMode type from ViModeIndicator, import from useViMode
- Add Ctrl+C handling in command mode to abort (standard vi behavior)
- Add test for Ctrl+C command mode abort

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Chunk 6 - Basic Movement Commands:
- Add h/j/k/l for character and line movement
- Add 0/$ for line start/end movement
- Export helper functions: getLineInfo, getLineCount, getLinePositions, moveCursor
- Handle boundary clamping and column preservation

Chunk 7 - Insert Mode Entry Commands:
- i: insert at cursor (existing, confirmed working)
- a: append after cursor
- A: append at end of line
- o: open line below
- O: open line above
- Wire up onContentChange callback for o/O operations

Completes Milestone B - navigation and insert mode now functional.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Chunk 8 - Undo Stack:
- Add internal undo stack with 100 entry limit
- pushUndoState captures content and cursor before edits
- 'u' command restores previous state
- Insert mode batches into single undo entry

Chunk 9 - Delete Commands:
- 'x' deletes character at cursor
- 'dd' deletes current line (pending operator pattern)
- Both push to undo stack before delete

Chunk 10 - Yank/Put Commands:
- 'yy' yanks current line to internal clipboard
- 'p' pastes after current line
- 'P' pastes before current line
- Clipboard persists within session (REQ-20, REQ-21)

Chunk 11 - Numeric Prefixes:
- Digits 1-9 start count accumulation
- '0' is line-start when no count, digit when accumulating
- Count applies to: h, l, j, k, x, dd, yy
- Large counts clamp to boundaries

Completes Milestone C - full Normal mode editing now functional.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The return value was set inside an async state setter callback,
making it unreliable. Since the return value was never used,
removed it entirely rather than leave a trap for future code.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…egration

Chunk 12 - Command Mode UI:
- Add ViCommandLine component for ex command display
- Show ":" prefix with command text and cursor indicator
- Style to match vim command line appearance

Chunk 13 - Ex Commands:
- :w saves file (calls onSave)
- :wq/:x saves and exits (calls onSave then onExit)
- :q triggers quit with unsaved check (calls onQuitWithUnsaved)
- :q! force quits (calls onExit directly)

Chunk 14 - Integration:
- Wire useViMode, useViCursor, useHasKeyboard into PairWritingEditor
- Add ViCursor overlay, ViModeIndicator, ViCommandLine components
- Gate on vault viMode config AND keyboard detection
- Connect callbacks to existing save/exit handlers

Chunk 15 - Polish:
- Escape in Normal mode clears pending operator and count
- ViCommandLine respects isProcessingQuickAction state
- Focus stays on textarea during command mode

Completes Milestone D - vi mode feature complete.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove onSubmit, onCancel, onChange props from ViCommandLine
  (these were never used; all input handled by useViMode)
- Remove dead code passing empty functions from PairWritingEditor
- Remove "Props API Tests" section testing non-existent props
- Wrap fireEvent calls in act() to eliminate React warnings
- Use separate act() calls for sequential keypresses in ex commands

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Jan 30, 2026

rjroy and others added 10 commits January 29, 2026 19:13
Add "Enable Vi Mode" checkbox in new "Editing" section of
ConfigEditorDialog, allowing users to toggle vi mode without
manually editing config files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The viMode field was missing from the initialConfig passed to
ConfigEditorDialog in both App.tsx and VaultSelect.tsx, causing
the value to not be saved when toggling the checkbox.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The viMode field was missing from local state updates after saving:
- VaultSelect.tsx: setVaults() wasn't updating viMode
- reducer.ts: UPDATE_VAULT_CONFIG wasn't applying viMode

This caused the checkbox to appear stuck after toggling because
the local state wasn't updated, so reopening the dialog showed
the old value.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed cursor background from --color-text (same as text color)
to --color-accent-quartary (Sunset Gold) so it's visible against
both the dark background and text. Also softened the blink animation
to fade between 0.85-0.3 opacity instead of completely disappearing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The cursor position was being calculated by subtracting textarea's
viewport position from the span's viewport position, but the mirror
element is positioned at -9999px off-screen. This resulted in very
negative coordinates.

Fixed by measuring the span position relative to the mirror element
instead, which gives correct offsets that work as the cursor position.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added scrollCursorIntoView() that calculates cursor line position and
adjusts textarea.scrollTop when the cursor moves outside the visible
area. Called after j/k movement commands to keep cursor visible.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The up-scroll was triggering too late (5-6 lines off-screen).
Added a 3-line margin so scrolling starts before the cursor
reaches the top edge, matching the responsive feel of down-scrolling.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous line-based calculation assumed one logical line = one
visual line, which broke with text wrapping. Now uses the same mirror
element technique as cursor positioning to get the actual pixel
position accounting for wrapped text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Focus the textarea on mount so users can start typing immediately
without clicking the text area first.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Captures lessons learned from the vi mode feature:
- Visual components need visual testing (cursor was invisible)
- Config changes must be traced end-to-end
- Text wrapping breaks line-based calculations
- Polish emerges from actual use

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@rjroy rjroy merged commit 7860c7b into main Jan 30, 2026
2 checks passed
@rjroy rjroy deleted the feat/394-vi-mode branch January 30, 2026 04:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pair Writing - vi mode

1 participant