Skip to content

fix(promptinput): keep bash-mode ! out of the local mirror (#1179)#1182

Open
0xghost42 wants to merge 1 commit into
Gitlawb:mainfrom
0xghost42:fix/1179-bang-mode-strip-mirror-sync
Open

fix(promptinput): keep bash-mode ! out of the local mirror (#1179)#1182
0xghost42 wants to merge 1 commit into
Gitlawb:mainfrom
0xghost42:fix/1179-bang-mode-strip-mirror-sync

Conversation

@0xghost42
Copy link
Copy Markdown
Contributor

Summary

Closes #1179. Typing ! into empty input is meant to enter bash mode and leave the prompt buffer empty (the ! only shows in the mode prefix). Before this patch, the ! stayed in the buffer and the cursor was wedged before it: e.g. !git status with the caret at position 0 instead of an empty buffer.

Root cause

useTextInput's default keystroke handler had a special case at the start of an empty buffer for the input-mode character:

if (cursor.isAtStart() && isInputModeCharacter(input)) {
  return cursor.insert(text).left()
}

That produced a cursor with text="!" and offset=0, which setValue("!", 0) then committed into the local mirror (liveValueRef="!", liveOffsetRef=0) and emitted as onChange("!").

PromptInput.detectModeEntry stripped the controlled parent value back to "" with cursor 0 — values the controlled props already held. Because the parent props ended up numerically identical to what they were before the keystroke, React did not re-render PromptInput, the useLayoutEffect in useTextInput never re-ran, and the local mirror kept ! at offset 0.

The next keystroke read liveValueRef="!" / liveOffsetRef=0, so inserts landed before the retained !, producing !git status with the cursor pinned at 0.

Fix

src/hooks/useTextInput.ts — at the same special case, emit onChange(text) as a one-shot mode-entry notification and return undefined so setValue is not called. The local mirror stays at "" / offset 0, the parent's strip remains a no-op on the controlled state, and the next character types into a clean buffer.

Test

New regression in src/components/TextInput.test.tsx:

  • Mounts a controlled TextInput with a parent onChange that mirrors PromptInput's strip (setValue('') when the new value starts with !).
  • Types ! then g then i then t.
  • Asserts the rendered frame contains git, and does NOT contain !, !git, or git!.

Before the fix the test fails (Received: "git!"). After the fix all 4 tests in the file pass.

Validation

  • bun test src/components/TextInput.test.tsx → 4/4 pass.
  • bun test src/components/PromptInput/ src/hooks/ → 24/24 pass.
  • bun run build clean.

Test plan

  • Manual: type !git status on Ubuntu Linux 24.04 — buffer renders git status, mode prefix shows !, cursor stays at end of typed text.
  • Manual: paste !ls -la into empty input — buffer renders ls -la, mode prefix shows !. (This path goes through detectModeEntry's multi-char branch and was already working.)
  • CI green.

…#1179)

Typing `!` into empty input is meant to enter bash mode and leave the
prompt buffer empty (the `!` shows in the mode prefix only). The
useTextInput special case at the default keystroke handler was
`cursor.insert(text).left()`, which placed `!` into the cursor text
with the offset at 0, then called `onChange("!")`. PromptInput's
`detectModeEntry` then stripped the controlled parent value back to
"" with cursor 0 — values it numerically already held.

Because the parent's controlled props ended up identical to what they
were before the keystroke, React did not re-render PromptInput, the
useLayoutEffect in useTextInput never re-ran, and the local mirror
retained `!` at offset 0. Subsequent keystrokes inserted before the
retained `!`, producing "!git status" with the cursor wedged before
the `!` instead of a clean "git status" buffer.

Fix is in useTextInput's default handler: when the keystroke is the
input-mode character at the start of an empty buffer, emit `onChange`
as a one-shot mode-entry notification but return `undefined` so
`setValue` is not called. The local mirror stays at "" / offset 0,
the parent strip remains a no-op on the controlled state, and the next
character is inserted into a clean buffer.

Test:
- New regression in TextInput.test.tsx that mounts a controlled
  TextInput with a parent `onChange` mirroring PromptInput's strip
  (return early with `setValue('')` when the new value starts with
  `!`), types `!` then `git`, and asserts the rendered frame
  contains `git` and does NOT contain `!`, `!git`, or `git!`.
- 4/4 in TextInput.test.tsx, 24/24 across PromptInput + hooks suites,
  build clean.
Copy link
Copy Markdown
Collaborator

@jatmn jatmn left a comment

Choose a reason for hiding this comment

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

Findings

  • [P2] Preserve the existing buffer when ! is typed at offset 0
    src/hooks/useTextInput.ts:488
    This branch now runs whenever the cursor is at offset 0, not only when the buffer is empty. If the prompt already contains git status, the user moves the cursor to the beginning, and then types ! to switch that command into bash mode, useTextInput now calls onChange('!') instead of sending the full !git status value. PromptInput.detectModeEntry then sees prevInputLength > 0 with a one-character value, does not enter bash mode, and the existing prompt is replaced with just !. That path is already documented as supported by the detectModeEntry test for prepending ! to non-empty existing text, so please restrict the one-shot empty-mirror path to an empty buffer or otherwise preserve the full inserted value for non-empty buffers.

@Vasanthdev2004
Copy link
Copy Markdown
Collaborator

Code Review

PR: #1182 — fix(promptinput): keep bash-mode ! out of the local mirror
Author: @0xghost42
Files changed: 2 (+81 -1)

Blockers

src/hooks/useTextInput.ts:488 — The fix breaks ! typing at position 0 on non-empty buffers.

The condition cursor.isAtStart() && isInputModeCharacter(input) runs whenever the cursor is at offset 0, not just when the buffer is empty. If the user has git status in the buffer, moves to position 0, and types !, the fix now calls onChange('!') instead of onChange('!git status'). This replaces the entire prompt with just !.

The previous reviewer (@jatmn) already flagged this — needs a cursor.isEmpty() guard or equivalent.

Non-Blocking

  • Test uses Bun.sleep(25) between keystrokes. Fine for now, but could be flaky on slow CI. Consistent with other tests in the file though.

Looks Good

  • Root cause analysis in the PR description is excellent — very clear explanation of the mirror desync issue.
  • The test component (BashModeStrippingTextInput) accurately mimics the real parent behavior.
  • Minimal, focused change — only touches what's needed.
  • Good inline comment explaining the "why" behind the fix.

Verdict: Changes Requested — the non-empty buffer regression is a real blocker.

@jatmn
Copy link
Copy Markdown
Collaborator

jatmn commented May 16, 2026

Please rebase before making changes, that should fix the smoke issues currently.

@gnanam1990
Copy link
Copy Markdown
Collaborator

Took an independent look — @jatmn's finding holds. The new branch in useTextInput.ts:488 keys off cursor.isAtStart() rather than an empty buffer, and emits onChange('!') with return undefined, so prepending ! to existing non-empty text (cursor moved to start of git status, then !) replaces the buffer with just ! instead of entering bash mode on the full value. One thing to add: the added regression test only exercises the empty-buffer path, so it wouldn't catch the non-empty-prepend regression @jatmn described — worth covering that case in the same test when you scope the fix to an empty buffer (or otherwise preserve the full inserted value). Happy to re-review once updated.

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.

"!" not getting removed, when executing terminal command

4 participants