Skip to content

fix: prevent Shift+Enter from triggering chat in new tab search input#628

Closed
shadowfax92 wants to merge 3 commits intomainfrom
fix/new-tab-shift-enter
Closed

fix: prevent Shift+Enter from triggering chat in new tab search input#628
shadowfax92 wants to merge 3 commits intomainfrom
fix/new-tab-shift-enter

Conversation

@shadowfax92
Copy link
Copy Markdown
Contributor

@shadowfax92 shadowfax92 commented Apr 1, 2026

Summary

  • Fixes Shift+Enter in the new tab search input starting a chat session instead of being ignored
  • Uses downshift's stateReducer to intercept InputKeyDownEnter when Shift is held, returning the current state (no-op)
  • Tracks shiftKey in a ref via the input's onKeyDown handler

Closes #560

Rework

Initial approach using preventDownshiftDefault on the SyntheticEvent didn't reliably prevent downshift from processing Enter. Switched to stateReducer, which is downshift's official customization mechanism and works at the state management level rather than the event level.

Test plan

  • Open a new tab, type text, press Shift+Enter — should NOT start a chat
  • Open a new tab, type text, press Enter — should still execute the default action (search/chat) as before
  • Open a new tab, type @ to trigger mention picker, press Tab — should still close mention picker
  • Verify sidepanel chat Shift+Enter still inserts newline (no regression)

🤖 Generated with Claude Code

Shift+Enter in the new tab combobox was being processed by downshift as
a regular Enter key, causing it to start a chat session. Guard the
onKeyDown handler to call preventDefault() when Shift is held, so
downshift skips the event entirely.

Closes #560
@github-actions github-actions bot added the fix label Apr 1, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR fixes a bug where pressing Shift+Enter in the new tab search input would incorrectly trigger a chat session. The fix introduces downshift's stateReducer to intercept InputKeyDownEnter actions when Shift is held, returning the current state unchanged (a no-op), and uses a shiftHeldRef to communicate the shift-key state from the onKeyDown handler into the reducer.

Key changes:

  • Adds shiftHeldRef (useRef(false)) to track whether Shift is held at the time of a keydown event.
  • Adds a stateReducer to useCombobox that returns the unmodified current state when InputKeyDownEnter fires with shiftHeldRef.current set, preventing downshift from processing the event.
  • Sets shiftHeldRef.current at the very start of onKeyDown, before the early-return guard for mentionState, ensuring the ref is always up-to-date when the reducer runs.

Correctness analysis:
The implementation is correct. Downshift's callAllEventHandlers calls the user-supplied onKeyDown first, then its own internal handler, so the ref is reliably set before the stateReducer is evaluated. When the reducer returns the current state, downshift detects no state change and skips both onStateChange and onSelectedItemChange, fully preventing executeDefaultAction() from being invoked. The previous approach using preventDownshiftDefault (noted in the PR) had a known reliability gap; using stateReducer is the correct, officially-supported mechanism.

Confidence Score: 5/5

Safe to merge — the fix is correct, uses downshift's official API, and has no impact on existing behavior.

No P0 or P1 issues found. The prior concern about preventDownshiftDefault reliability has been fully addressed by switching to stateReducer. The ref-based timing is sound (onKeyDown fires before the internal dispatch), and returning the current state from the reducer correctly suppresses both onStateChange and onSelectedItemChange. All remaining observations are P2 or lower.

No files require special attention.

Important Files Changed

Filename Overview
packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTab.tsx Adds stateReducer to useCombobox and a shiftHeldRef to correctly ignore Shift+Enter — approach is sound and addresses the previous concern about preventDownshiftDefault.

Sequence Diagram

sequenceDiagram
    participant User
    participant onKeyDown as onKeyDown handler
    participant shiftRef as shiftHeldRef
    participant Downshift as Downshift internals
    participant stateReducer as stateReducer
    participant Callbacks as onStateChange/onSelectedItemChange

    User->>onKeyDown: Shift+Enter keydown
    onKeyDown->>shiftRef: set current to true
    onKeyDown->>Downshift: callAllEventHandlers continues
    Downshift->>stateReducer: InputKeyDownEnter action
    stateReducer->>shiftRef: read current (true)
    stateReducer-->>Downshift: return state (no-op)
    Note over Downshift,Callbacks: No state change, callbacks skipped
    Note over Callbacks: executeDefaultAction blocked

    User->>onKeyDown: Enter keydown (no Shift)
    onKeyDown->>shiftRef: set current to false
    onKeyDown->>Downshift: callAllEventHandlers continues
    Downshift->>stateReducer: InputKeyDownEnter action
    stateReducer->>shiftRef: read current (false)
    stateReducer-->>Downshift: return changes (normal flow)
    Downshift->>Callbacks: state change applied
    Note over Callbacks: executeDefaultAction fires
Loading

Reviews (2): Last reviewed commit: "fix: use stateReducer to block Shift+Ent..." | Re-trigger Greptile

Comment on lines +495 to +498
if (e.key === 'Enter' && e.shiftKey) {
e.preventDefault()
return
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 e.preventDefault() does not stop downshift's internal handler

e.preventDefault() prevents browser-native defaults (e.g. form submission) but does not prevent downshift from processing the key event. Downshift's getInputProps uses a callAllEventHandlers utility that calls both the user-supplied handler and its own internal handler sequentially. It stops only when event.nativeEvent.preventDownshiftDefault is true — it does not check event.defaultPrevented.

Because of this, even after your guard returns, downshift's internal handler still fires, sees Enter, and can:

  1. Select the currently highlighted suggestion → onSelectedItemChangerunSelectedAction → starts a chat
  2. Dispatch InputKeyDownEnter to onStateChange under certain conditions

The fix needs to additionally set the downshift-specific flag:

Suggested change
if (e.key === 'Enter' && e.shiftKey) {
e.preventDefault()
return
}
if (e.key === 'Enter' && e.shiftKey) {
e.preventDefault()
;(e.nativeEvent as any).preventDownshiftDefault = true
return
}

Without preventDownshiftDefault = true, Shift+Enter will still be processed by downshift whenever a suggestion is highlighted, and the original bug is likely to persist.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/agent/entrypoints/newtab/index/NewTab.tsx
Line: 495-498

Comment:
**`e.preventDefault()` does not stop downshift's internal handler**

`e.preventDefault()` prevents browser-native defaults (e.g. form submission) but does **not** prevent downshift from processing the key event. Downshift's `getInputProps` uses a `callAllEventHandlers` utility that calls both the user-supplied handler and its own internal handler sequentially. It stops only when `event.nativeEvent.preventDownshiftDefault` is `true` — it does **not** check `event.defaultPrevented`.

Because of this, even after your guard returns, downshift's internal handler still fires, sees `Enter`, and can:
1. Select the currently highlighted suggestion → `onSelectedItemChange``runSelectedAction` → starts a chat
2. Dispatch `InputKeyDownEnter` to `onStateChange` under certain conditions

The fix needs to additionally set the downshift-specific flag:

```suggestion
                      if (e.key === 'Enter' && e.shiftKey) {
                        e.preventDefault()
                        ;(e.nativeEvent as any).preventDownshiftDefault = true
                        return
                      }
```

Without `preventDownshiftDefault = true`, Shift+Enter will still be processed by downshift whenever a suggestion is highlighted, and the original bug is likely to persist.

How can I resolve this? If you propose a fix, please make it concise.

preventDefault() doesn't stop downshift's internal Enter key handling.
Downshift checks event.nativeEvent.preventDownshiftDefault to decide
whether to skip its built-in behavior.
preventDownshiftDefault on the SyntheticEvent doesn't reliably prevent
downshift from processing Enter. Switch to stateReducer, which is
downshift's official customization mechanism: track shiftKey in a ref
via onKeyDown, then return current state from the reducer when
InputKeyDownEnter fires with Shift held.
@shadowfax92
Copy link
Copy Markdown
Contributor Author

@greptileai review

@shadowfax92
Copy link
Copy Markdown
Contributor Author

@claude review

@claude
Copy link
Copy Markdown

claude bot commented Apr 1, 2026

Claude finished @shadowfax92's task in 1m 44s —— View job


Code Review

  • Gather context
  • Read changed file
  • Analyze implementation
  • Post review feedback

Summary

The Greptile review is outdated — it analyzed commit 515f90f (which used e.preventDefault()). The current HEAD (88ea297) already switched to stateReducer, which is exactly the fix Greptile recommended. The core concern has been addressed.


Analysis of Current Implementation (88ea297)

The approach is correct. Using stateReducer is the right mechanism here — it intercepts at downshift's state management level, so it completely vetoes InputKeyDownEnter before onSelectedItemChange or onStateChange can fire. No chat will be started.

Timing is sound. In getInputProps, downshift merges handlers via callAllEventHandlers, calling the user's handler first. So shiftHeldRef.current = e.shiftKey (line 506) runs before downshift's internal handler, and stateReducer (called synchronously in downshift's useReducer dispatch) reads the correct value.

onStateChange is suppressed. When stateReducer returns state unchanged, downshift detects no state diff and does not invoke onStateChange, so executeDefaultAction() at line 207 is also blocked.


Minor Observations

  1. shiftHeldRef placement is correct. It's set before the !mentionStateRef.current.isOpen early return (line 507), so Shift+Enter is also blocked when the mention picker is open — which is the right behavior.

  2. onStateChange guard at line 205–209 has a subtle condition:

    if (!selectedItem && !highlightedIndex && !stateInputValue)

    !highlightedIndex is falsy when highlightedIndex === 0 (first item highlighted), which seems like a pre-existing edge case unrelated to this PR.

  3. No cleanup needed. The ref correctly self-resets on every keydown event — no stale state possible.


Verdict

The implementation is correct and uses the right mechanism. The stateReducer approach works at the state management level as intended. Ready to merge pending test plan verification.

@shadowfax92 shadowfax92 closed this Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Shift+Enter on new tab starts chat instead of inserting a newline

1 participant