Skip to content

feat(tui): vim motions in prompt input#12679

Open
leohenon wants to merge 20 commits intoanomalyco:devfrom
leohenon:feat/vim-prompt-input-core
Open

feat(tui): vim motions in prompt input#12679
leohenon wants to merge 20 commits intoanomalyco:devfrom
leohenon:feat/vim-prompt-input-core

Conversation

@leohenon
Copy link

@leohenon leohenon commented Feb 8, 2026

What does this PR do?

Closes #1764
Closes #11111

Adds optional Vim motions to the prompt input. Enable with tui.vim: true or toggle from the menu. Disabled by default.

Screenshot 2026-02-08 at 5 34 03 PM

Supported:

  • Mode switching: i I a A o O S, cc, cw, Esc
  • Motions: h j k l, w b e, W B E, 0 ^ $
  • Deletes: x, dd, dw
  • Session navigation: gg/G
  • Scrolling: Ctrl+e/y/d/u/f/b
  • Enter in normal mode submits

Motivation: I use OpenCode daily and the external editor round-trip for small edits was slow. I originally built this for personal use and then cleaned it up for upstream.

Implementation: Vim state isolated in /vim module. Handler owns mode + pending operator state. Mode resets to insert after submit. No visual mode, intentionally excluded to avoid conflicts with the clipboard system.

I saw thdxr's concern in #1764 about partial Vim feeling incomplete. This PR covers only the core motions needed for prompt editing and is intentionally kept small to make review manageable. I have a fuller implementation locally (yank/put, word/line selections like daw/diw, yaw/yiw, history cycling with j/k, and menu navigation) that I can split into follow-up PRs if this direction looks good.

How did you verify your code works?

Used the fuller implementation locally for over a week of daily use. Reimplemented the core subset for this PR and verified manually (editing, scrolling, session switching, edge cases with pending operators, empty buffers, multiline). Added targeted tests for mode transitions, motions, deletes, pending state, and submit reset. All passing.

Copilot AI review requested due to automatic review settings February 8, 2026 09:43
@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

Hey! Your PR title vim motions in prompt input box doesn't follow conventional commit format.

Please update it to start with one of:

  • feat: or feat(scope): new feature
  • fix: or fix(scope): bug fix
  • docs: or docs(scope): documentation changes
  • chore: or chore(scope): maintenance tasks
  • refactor: or refactor(scope): code refactoring
  • test: or test(scope): adding or updating tests

Where scope is the package name (e.g., app, desktop, opencode).

See CONTRIBUTING.md for details.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 8, 2026

The following comment was made by an LLM, it may be inaccurate:

No duplicate PRs found

@leohenon leohenon changed the title vim motions in prompt input box feat(tui): vim motions in prompt input box Feb 8, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional Vim-style keybinding layer for the TUI prompt input (configurable via tui.vim or a settings toggle), including a mode indicator, motion/operator handling, and related documentation/tests.

Changes:

  • Introduces a /vim module (state, handler, motions, scroll/jump mapping, indicator) and wires it into the prompt input flow.
  • Adds a command-palette setting to toggle Vim input, plus config schema + docs updates.
  • Adds a targeted test suite covering mode transitions, motions, operators, scrolling, and jumps.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/web/src/content/docs/tui.mdx Documents Vim mode and supported keys for the TUI prompt.
packages/web/src/content/docs/config.mdx Documents tui.vim config flag.
packages/opencode/src/config/config.ts Adds tui.vim boolean to config schema.
packages/opencode/src/cli/cmd/tui/component/vim/vim-state.ts New Vim mode/pending state container.
packages/opencode/src/cli/cmd/tui/component/vim/vim-scroll.ts Maps Ctrl+key combos to scroll actions.
packages/opencode/src/cli/cmd/tui/component/vim/vim-motions.ts Implements cursor motions + delete/edit primitives on the textarea.
packages/opencode/src/cli/cmd/tui/component/vim/vim-motion-jump.ts Implements gg/G jump behavior.
packages/opencode/src/cli/cmd/tui/component/vim/vim-indicator.ts Computes INSERT/NORMAL indicator text.
packages/opencode/src/cli/cmd/tui/component/vim/vim-handler.ts Key-event interpreter coordinating state + motions + submit/scroll/jump.
packages/opencode/src/cli/cmd/tui/component/vim/index.ts Provides useVimEnabled() integration with KV + config.
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx Integrates Vim handler/state into prompt input, cursor styling, submit behavior, and indicator rendering.
packages/opencode/src/cli/cmd/tui/app.tsx Adds command-palette toggle to enable/disable Vim input.
packages/opencode/test/cli/tui/vim-motions.test.ts Adds tests for handler behavior, operators, and scroll mapping.
Comments suppressed due to low confidence (1)

packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:918

  • The inline comment says "Don't preventDefault" but the code immediately calls e.preventDefault(). Either update the comment to match the behavior or adjust the behavior if the intent really is to let the textarea handle the event.
                    await exit()
                    // Don't preventDefault - let textarea potentially handle the event
                    e.preventDefault()
                    return

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 102 to 106
let pos = offset
if (pos < text.length) pos++
while (pos < text.length && !match(text[pos])) pos++
while (pos < text.length && match(text[pos])) pos++
if (pos > offset + 1) pos--
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wordEnd() currently increments pos before checking whether the cursor is already on a word character. This makes e/E behave incorrectly for 1-character words or when the cursor is on the last character of a word followed by whitespace (e.g., in "a b", e from a will jump to b, and in "a" it can return text.length, which then breaks normal-mode assumptions like l clamping). Consider rewriting wordEnd to first find the end of the current word when on a word char, and ensure it never returns an offset past the last character when used for normal-mode motions; add a regression test for the 1-character word case.

Suggested change
let pos = offset
if (pos < text.length) pos++
while (pos < text.length && !match(text[pos])) pos++
while (pos < text.length && match(text[pos])) pos++
if (pos > offset + 1) pos--
// If the buffer is empty, stay at 0.
if (text.length === 0) return 0
// Clamp starting position to the last valid character index.
let pos = offset
if (pos >= text.length) pos = text.length - 1
// If we're on a word character, move to the end of the *current* word.
if (match(text[pos])) {
while (pos + 1 < text.length && match(text[pos + 1])) pos++
return pos
}
// Otherwise, skip forward to the start of the next word (if any)...
while (pos < text.length && !match(text[pos])) pos++
if (pos >= text.length) return text.length - 1
// ...then move to the end of that word.
while (pos + 1 < text.length && match(text[pos + 1])) pos++

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment on lines 6 to 10
export function createVimState(input: {
enabled: Accessor<boolean>
active: Accessor<boolean>
initial?: Accessor<VimMode | undefined>
}) {
Copy link

Copilot AI Feb 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createVimState requires an active accessor in its input but never reads it. Either remove active from the API (to avoid misleading callers) or use it to reset mode/pending when the prompt becomes inactive (e.g., hidden/disabled), since callers are already computing that signal.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

@leohenon leohenon changed the title feat(tui): vim motions in prompt input box feat(tui): vim motions in prompt input Feb 8, 2026
@leohenon leohenon force-pushed the feat/vim-prompt-input-core branch from 2c180c9 to 87b4487 Compare February 8, 2026 13:28
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.

[FEATURE]: VIM Keyboard Layout [FEATURE]: vim motions in input box

1 participant