Skip to content

feat: add custom AppMenu component for windows & linux#6934

Merged
bijin-bruno merged 5 commits intousebruno:mainfrom
naman-bruno:bugfix/window-toolbar
Jan 30, 2026
Merged

feat: add custom AppMenu component for windows & linux#6934
bijin-bruno merged 5 commits intousebruno:mainfrom
naman-bruno:bugfix/window-toolbar

Conversation

@naman-bruno
Copy link
Collaborator

@naman-bruno naman-bruno commented Jan 27, 2026

Description

Recording.2026-01-27.122358.mp4

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

  • New Features

    • App menu added to the title bar with File, Edit, View, and Help menus.
    • Menu actions now open Preferences, Docs, About (shows version), toggle DevTools, zoom controls, and fullscreen.
    • Nested submenus with hover-to-open, keyboard navigation, and configurable submenu placement.
    • Dropdowns now support mouse enter/leave to improve hover behavior.
  • Style

    • Adjusted title bar spacing and submenu arrow/item styling for improved layout and readability.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 27, 2026

Walkthrough

Adds a title-bar AppMenu component and its styling, extends Dropdown/MenuDropdown to support hover-driven nested submenus, and registers multiple ipcMain handlers in the Electron main process for renderer-initiated actions (preferences, devtools, zoom, fullscreen, docs, about).

Changes

Cohort / File(s) Summary
AppMenu UI Components
packages/bruno-app/src/components/AppTitleBar/AppMenu/StyledWrapper.js, packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js
New styled wrapper and AppMenu component implementing top-level menus (File, Edit, View, Help) with nested submenus, keyboard shortcuts, local open/close state, and ipcRenderer/document command actions.
AppTitleBar Integration
packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js, packages/bruno-app/src/components/AppTitleBar/index.js
Adds .app-menu left margin and imports/renders AppMenu inside the title bar when showWindowControls is true.
Dropdown & MenuDropdown
packages/bruno-app/src/components/Dropdown/StyledWrapper.js, packages/bruno-app/src/components/Dropdown/index.js, packages/bruno-app/src/ui/MenuDropdown/index.js, packages/bruno-app/src/ui/MenuDropdown/SubMenuItem/index.js
Adds submenu-related CSS selectors, forwards onMouseEnter/onMouseLeave in Dropdown, implements SubMenuItem and hover-activated nested submenus in MenuDropdown, adds submenuPlacement prop, and updates click-outside handling for nested tippy roots.
Electron Main IPC
packages/bruno-electron/src/index.js
Adds ipcMain.handle handlers for renderer:open-preferences, renderer:toggle-devtools, renderer:reset-zoom, renderer:zoom-in, renderer:zoom-out, renderer:toggle-fullscreen, renderer:open-docs, and renderer:open-about to perform main-process actions or emit main events.

Sequence Diagram

sequenceDiagram
    participant User
    participant Renderer as AppMenu (Renderer)
    participant IPC as ipcRenderer
    participant Main as Main Process
    participant App as Electron App

    User->>Renderer: hover / click menu item
    Renderer->>IPC: ipcRenderer.invoke('renderer:*')
    IPC->>Main: main handler executes
    alt open-preferences / open-docs / open-about
        Main->>App: emit event / open about window
    else toggle-devtools / zoom-* / toggle-fullscreen
        Main->>App: toggle devtools / adjust zoom / toggle fullscreen
    end
    Main-->>IPC: resolve
    IPC-->>Renderer: response
    Renderer-->>User: update UI / close menu
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • helloanoop
  • lohit-bruno
  • bijin-bruno

Poem

A menu blossoms on the title bar,
Hover sparks wings of nested flair.
Renderer whispers through IPC streams,
Main replies and fulfills the dreams.
A tiny feature, pleased and rare. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding a custom AppMenu component specifically for Windows and Linux platforms.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js`:
- Around line 173-175: The root StyledWrapper in the AppMenu component is
missing the "app-menu" class so the margin rule in AppTitleBar's StyledWrapper
doesn't apply; update the JSX for StyledWrapper (the element using menuRef in
the AppMenu component) to include className="app-menu" (or merge it with any
existing className prop) so the spacing rule targets this root wrapper.
- Around line 16-29: The hover timeout created in handleMouseLeave uses
timeoutRef.current and can call setIsOpen after the component unmounts; update
the component to clear the timeout on unmount by adding a useEffect cleanup that
checks timeoutRef.current and calls clearTimeout(timeoutRef.current) (and nulls
it) to prevent stale setState, and also consider clearing timeoutRef.current
before setting a new timeout in handleMouseLeave or when opening (to avoid
overlapping timers); reference handleMouseLeave, timeoutRef, setIsOpen, and
submenuItemsWithClose/onItemClick when making the change.
- Around line 11-49: The submenu closes prematurely because handleMouseLeave
sets a 50ms timeout while the popper is appended to document.body; fix by
preventing close when hovering the popper: add matching mouse handlers on the
popper content provided to MenuDropdown (or switch submenu trigger to
click-to-open) that call clearTimeout(timeoutRef.current) on mouse enter and set
the same timeout-based close on mouse leave, and ensure you
clearTimeout(timeoutRef.current) in a useEffect cleanup to avoid state updates
after unmount; update the MenuDropdown usage (appendTo={() => document.body}) to
pass those handlers to the popper wrapper so setIsOpen and timeoutRef are
coordinated with handleMouseEnter/handleMouseLeave.
🧹 Nitpick comments (1)
packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js (1)

60-170: Remove optional chaining on ipcRenderer in renderer code.
The repo convention is to treat window.ipcRenderer as always available in packages/bruno-app and avoid guards. Based on learnings, align these calls accordingly.

♻️ Proposed fix
-  const { ipcRenderer } = window;
+  const { ipcRenderer } = window;
@@
-      onClick: () => ipcRenderer?.invoke('renderer:open-collection')
+      onClick: () => ipcRenderer.invoke('renderer:open-collection')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:open-preferences')
+      onClick: () => ipcRenderer.invoke('renderer:open-preferences')
@@
-      onClick: () => ipcRenderer?.send('renderer:window-close')
+      onClick: () => ipcRenderer.send('renderer:window-close')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:toggle-devtools')
+      onClick: () => ipcRenderer.invoke('renderer:toggle-devtools')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:reset-zoom')
+      onClick: () => ipcRenderer.invoke('renderer:reset-zoom')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:zoom-in')
+      onClick: () => ipcRenderer.invoke('renderer:zoom-in')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:zoom-out')
+      onClick: () => ipcRenderer.invoke('renderer:zoom-out')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:toggle-fullscreen')
+      onClick: () => ipcRenderer.invoke('renderer:toggle-fullscreen')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:open-about')
+      onClick: () => ipcRenderer.invoke('renderer:open-about')
@@
-      onClick: () => ipcRenderer?.invoke('renderer:open-docs')
+      onClick: () => ipcRenderer.invoke('renderer:open-docs')
Based on learnings, align renderer IPC usage to unguarded calls.

Comment on lines +11 to +49
const handleMouseEnter = () => {
clearTimeout(timeoutRef.current);
setIsOpen(true);
};

const handleMouseLeave = () => {
timeoutRef.current = setTimeout(() => setIsOpen(false), 50);
};

const submenuItemsWithClose = submenuItems.map((item) => {
if (item.type === 'divider') return item;
return {
...item,
onClick: () => {
item.onClick?.();
onItemClick();
}
};
});

return (
<div
className="submenu-trigger"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<MenuDropdown
items={submenuItemsWithClose}
placement="right-start"
opened={isOpen}
onChange={setIsOpen}
showTickMark={false}
appendTo={() => document.body}
>
<div className="submenu-trigger-content">
<span>{label}</span>
<IconChevronRight size={14} />
</div>
</MenuDropdown>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the AppMenu file
find . -type f -name "index.js" | grep -i appmenu | head -5

Repository: usebruno/bruno

Length of output: 122


🏁 Script executed:

# Search for the AppMenu/SubmenuTrigger file
fd -t f "index.js" -p "*AppMenu*"

Repository: usebruno/bruno

Length of output: 154


🏁 Script executed:

# Let's look for the file in packages/bruno-app
find packages/bruno-app -type f -path "*AppTitleBar/AppMenu*" -name "*.js"

Repository: usebruno/bruno

Length of output: 191


🏁 Script executed:

# Broader search for AppMenu
find packages/bruno-app -type f -name "*.js" | xargs grep -l "SubmenuTrigger\|handleMouseEnter.*handleMouseLeave" | head -5

Repository: usebruno/bruno

Length of output: 120


🏁 Script executed:

# Look at the structure to understand the path
ls -la packages/bruno-app/src/components/AppTitleBar/ 2>/dev/null || echo "Path not found, searching..."
find packages/bruno-app -type d -name "AppMenu" 2>/dev/null

Repository: usebruno/bruno

Length of output: 414


🏁 Script executed:

# Read the AppMenu index.js file to see the full context
cat -n packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js | head -80

Repository: usebruno/bruno

Length of output: 2812


🏁 Script executed:

# Let's also check if MenuDropdown is a component in the repo
find packages -type f -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*MenuDropdown" | head -5

Repository: usebruno/bruno

Length of output: 255


🏁 Script executed:

# Examine the MenuDropdown component
cat -n packages/bruno-app/src/ui/MenuDropdown/index.js | head -150

Repository: usebruno/bruno

Length of output: 7601


🏁 Script executed:

# Let's see the full file size first
wc -l packages/bruno-app/src/ui/MenuDropdown/index.js

Repository: usebruno/bruno

Length of output: 109


🏁 Script executed:

# Continue reading the MenuDropdown to find event handling, especially for popper/dropdown
cat -n packages/bruno-app/src/ui/MenuDropdown/index.js | sed -n '150,300p'

Repository: usebruno/bruno

Length of output: 5877


🏁 Script executed:

# Check if there's any hover handling in the Dropdown component it wraps
find packages/bruno-app/src/components -name "Dropdown*" -o -name "dropdown*" | head -5

Repository: usebruno/bruno

Length of output: 100


🏁 Script executed:

# Check the Dropdown component implementation
cat -n packages/bruno-app/src/components/Dropdown/index.js | head -150

Repository: usebruno/bruno

Length of output: 1179


🏁 Script executed:

# Check if Dropdown is a Tippy wrapper
wc -l packages/bruno-app/src/components/Dropdown/index.js

Repository: usebruno/bruno

Length of output: 112


🏁 Script executed:

# Continue reading MenuDropdown to see if it adds hover handlers to prevent closing
cat -n packages/bruno-app/src/ui/MenuDropdown/index.js | sed -n '300,400p'

Repository: usebruno/bruno

Length of output: 4337


🏁 Script executed:

# Check the end of MenuDropdown to see the render and any event handlers
cat -n packages/bruno-app/src/ui/MenuDropdown/index.js | sed -n '400,467p'

Repository: usebruno/bruno

Length of output: 2381


🏁 Script executed:

# Check if SubmenuTrigger has any cleanup for the timeout on unmount
sed -n '7,52p' packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js

Repository: usebruno/bruno

Length of output: 1175


🏁 Script executed:

# Let's also check how the StyledWrapper is used in the AppMenu
cat -n packages/bruno-app/src/components/AppTitleBar/AppMenu/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 1342


🏁 Script executed:

# Check if there are any mouse event handlers on the popper or menu container in MenuDropdown that might prevent closing
grep -n "onMouse\|onPointer" packages/bruno-app/src/ui/MenuDropdown/index.js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Let's verify the behavior one more time by checking if there's any popper-level hover prevention
cat -n packages/bruno-app/src/components/Dropdown/StyledWrapper.js 2>/dev/null || echo "File not found, checking structure..."

Repository: usebruno/bruno

Length of output: 6125


🏁 Script executed:

# Check if there are any parent wrappers around the popper that might have hover handlers
find packages/bruno-app/src/components/Dropdown -type f -name "*.js"

Repository: usebruno/bruno

Length of output: 169


Submenu closes when cursor moves from trigger to popper.
The onMouseLeave on the trigger fires as the cursor enters the popper (appended to document.body), setting a 50ms timeout that closes the menu before the user can interact with items. Add mouse event handlers to the popper to prevent closing while hovering it, or switch to click-to-open for submenus.

Also, the timeout should be cleaned up on unmount to prevent state updates after the component unmounts.

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js` around lines
11 - 49, The submenu closes prematurely because handleMouseLeave sets a 50ms
timeout while the popper is appended to document.body; fix by preventing close
when hovering the popper: add matching mouse handlers on the popper content
provided to MenuDropdown (or switch submenu trigger to click-to-open) that call
clearTimeout(timeoutRef.current) on mouse enter and set the same timeout-based
close on mouse leave, and ensure you clearTimeout(timeoutRef.current) in a
useEffect cleanup to avoid state updates after unmount; update the MenuDropdown
usage (appendTo={() => document.body}) to pass those handlers to the popper
wrapper so setIsOpen and timeoutRef are coordinated with
handleMouseEnter/handleMouseLeave.

Comment on lines +173 to +175
return (
<StyledWrapper ref={menuRef}>
<MenuDropdown
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

.app-menu spacing won’t apply without a matching class.
Add the class to the root wrapper so the margin rule in AppTitleBar’s StyledWrapper takes effect.

🧩 Proposed fix
-    <StyledWrapper ref={menuRef}>
+    <StyledWrapper ref={menuRef} className="app-menu">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<StyledWrapper ref={menuRef}>
<MenuDropdown
return (
<StyledWrapper ref={menuRef} className="app-menu">
<MenuDropdown
🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js` around lines
173 - 175, The root StyledWrapper in the AppMenu component is missing the
"app-menu" class so the margin rule in AppTitleBar's StyledWrapper doesn't
apply; update the JSX for StyledWrapper (the element using menuRef in the
AppMenu component) to include className="app-menu" (or merge it with any
existing className prop) so the spacing rule targets this root wrapper.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js`:
- Around line 74-113: The editItems menu currently uses deprecated
document.execCommand (see editItems and the entries with id
'undo','redo','cut','copy','paste','select-all'), which won't work for Bruno's
CodeMirror editors; update each onClick to delegate to the active editor
instance (e.g. find the focused CodeMirror v5 instance and call its APIs like
cm.undo(), cm.redo(), cm.cut/replaceSelection or use clipboard/selection APIs
for copy/paste, and cm.execCommand equivalents) or route the action through the
same IPC handler pattern used by the File menu / menu-template.js so the main
process or editor context performs the operation on the correct editor instead
of calling document.execCommand.

Comment on lines +74 to +113
const editItems = [
{
id: 'undo',
label: 'Undo',
rightSection: <span className="shortcut">Ctrl+Z</span>,
onClick: () => document.execCommand('undo')
},
{
id: 'redo',
label: 'Redo',
rightSection: <span className="shortcut">Ctrl+Y</span>,
onClick: () => document.execCommand('redo')
},
{ type: 'divider', id: 'edit-div-1' },
{
id: 'cut',
label: 'Cut',
rightSection: <span className="shortcut">Ctrl+X</span>,
onClick: () => document.execCommand('cut')
},
{
id: 'copy',
label: 'Copy',
rightSection: <span className="shortcut">Ctrl+C</span>,
onClick: () => document.execCommand('copy')
},
{
id: 'paste',
label: 'Paste',
rightSection: <span className="shortcut">Ctrl+V</span>,
onClick: () => document.execCommand('paste')
},
{ type: 'divider', id: 'edit-div-2' },
{
id: 'select-all',
label: 'Select All',
rightSection: <span className="shortcut">Ctrl+A</span>,
onClick: () => document.execCommand('selectAll')
}
];
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

document.execCommand deprecated status 2024 2025

💡 Result:

As of 2024–2025, document.execCommand() is deprecated/obsolete for web-platform use:

  • MDN marks Document.execCommand() as “Deprecated” and explicitly recommends avoiding it for new code, warning it may be dropped from standards and kept only for compatibility. MDN also notes there are still some use cases with no viable alternatives (notably around preserving the undo stack). [1]
  • The W3C Web Editing WG “execCommand” spec is a draft that is not expected to advance, and states the features are inconsistently implemented across browsers and not expected to become consistent; it also notes there’s currently no alternative for some actions (especially clipboard- and editing-related). [2]
  • For clipboard operations, guidance has shifted toward the Async Clipboard API (navigator.clipboard.*) rather than execCommand('copy'/'cut'/'paste'), though capabilities and constraints differ. [3]

Practical takeaway (2024–2025): expect execCommand() to remain widely implemented for compatibility, but it’s effectively a legacy API—avoid it in new work where possible, and prefer targeted modern APIs (e.g., Clipboard API) or editor libraries for rich-text editing. [1] [2] [3]

Sources:
[1] MDN – Document.execCommand() (Deprecated) https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
[2] W3C Web Editing WG – execCommand draft (not expected to advance) https://w3c.github.io/editing/docs/execCommand/
[3] MDN – Clipboard transition away from document.execCommand toward navigator.clipboard https://developer.mozilla.org/en-US/docs/Add-ons/WebExtensions/Interact_with_the_clipboard


🌐 Web query:

Bruno app Electron code editor CodeMirror Monaco

💡 Result:

Bruno Desktop is an Electron app (the packages/bruno-electron/package.json lists electron in devDependencies). [1]

For its in-app code/text editors, Bruno uses CodeMirror (v5), not Monaco:

  • packages/bruno-app/package.json depends on codemirror (e.g., codemirror: 5.65.2). [2]
  • Bruno’s editor components instantiate CodeMirror directly (e.g., const CodeMirror = require('codemirror'); and CodeMirror(...)). [3]

Monaco Editor is not part of Bruno’s editor stack based on these dependencies/usages. [2][3]

References:
[1] (fossies.org)
[2] (fossies.org)
[3] (fossies.org)

Citations:


🏁 Script executed:

# Check what editor Bruno uses and how it handles undo/redo/copy/paste
cd packages/bruno-app && find . -type f -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" | head -20

Repository: usebruno/bruno

Length of output: 887


🏁 Script executed:

# Search for editor implementation in Bruno
rg -i "codemirror|monaco|editor" packages/bruno-app/src --type js --type jsx --max-count=10

Repository: usebruno/bruno

Length of output: 85


🏁 Script executed:

# Check the AppMenu file context to understand how these commands are used
cat -n packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js | head -120

Repository: usebruno/bruno

Length of output: 4198


🏁 Script executed:

# Search for how Bruno's CodeMirror editor handles undo/redo/copy/paste operations
rg "undo|redo|copy|paste" packages/bruno-app/src --type js -i -B 2 -A 2 | head -80

Repository: usebruno/bruno

Length of output: 7279


🏁 Script executed:

# Check if there's any event delegation or focus management in the AppMenu or editors
rg "document\.execCommand|contentEditable" packages/bruno-app/src --type js

Repository: usebruno/bruno

Length of output: 740


🏁 Script executed:

# Look for CodeMirror editor instantiation and its undo/redo handling
fd ".*Editor.*" packages/bruno-app/src --type f -name "*.js" | head -10

Repository: usebruno/bruno

Length of output: 288


🏁 Script executed:

# Check CodeMirror plugin usage in Bruno
rg "codemirror" packages/bruno-app/src --type js -B 1 -A 1 | head -60

Repository: usebruno/bruno

Length of output: 4394


🏁 Script executed:

# Check if there's any focus management or editor reference in AppMenu
rg "focus|editor|codeMirror" packages/bruno-app/src/components/AppTitleBar/AppMenu --type js -i

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Look for how CodeMirror editors handle undo/redo
rg "\.undo\(\)|\.redo\(\)" packages/bruno-app/src --type js | head -20

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check if there's a custom implementation for these edit commands elsewhere
rg "undo|redo" packages/bruno-app/src/components --type js -i --max-count=15

Repository: usebruno/bruno

Length of output: 944


🏁 Script executed:

# Check electron/main process for keyboard shortcuts handling
fd "main.js" packages/bruno-electron --type f -exec cat {} \; | head -100

Repository: usebruno/bruno

Length of output: 204


🏁 Script executed:

# Check if AppMenu or any parent component has access to CodeMirror instance
rg "editorInstance|codeMirrorInstance|getEditor" packages/bruno-app/src/components --type js -B 2 -A 2

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check how other parts of Bruno integrate menu actions with CodeMirror
rg "MenuDropdown|onClick.*editor" packages/bruno-app/src/components --type js -B 1 -A 1 | head -40

Repository: usebruno/bruno

Length of output: 3381


🏁 Script executed:

# Look for integration tests or usage patterns of AppMenu
find packages/bruno-app/src -name "*.spec.js" -o -name "*.test.js" | xargs grep -l "AppMenu" 2>/dev/null

Repository: usebruno/bruno

Length of output: 42


🏁 Script executed:

# Check if there's Redux/state management that connects AppMenu to editors
rg "dispatch.*undo|dispatch.*redo" packages/bruno-app/src --type js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check CodeMirror documentation for undo/redo/copy/paste API
rg "historySize|clearHistory" packages/bruno-app/src --type js | head -10

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Look for editor initialization and method calls in the request/response editors
fd "Editor" packages/bruno-app/src/components -type f -name "*.js" | head -5 | xargs grep -l "CodeMirror" 2>/dev/null

Repository: usebruno/bruno

Length of output: 230


🏁 Script executed:

# Check specifically how RequestPane or MultiLineEditor handles undo/redo
cat packages/bruno-app/src/components/MultiLineEditor/index.js 2>/dev/null | head -150

Repository: usebruno/bruno

Length of output: 5102


🏁 Script executed:

# Check if AppMenu component tracks focus or has ref to active editor
rg "querySelector|focus|activeElement" packages/bruno-app/src/components/AppTitleBar/AppMenu --type js -B 2 -A 2

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Look for any electron preload or IPC handlers for undo/redo
find packages/bruno-electron/src -name "*.js" -type f | head -10

Repository: usebruno/bruno

Length of output: 547


🏁 Script executed:

# Search for how CodeMirror undo/redo is actually exposed/used in Bruno
rg "\.undo\(|\.redo\(" packages/bruno-app/src --type js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check if there's a workaround for menu-based undo/redo in any component
rg "execCommand|undo|redo" packages/bruno-app/src/components --type js -A 3 -B 3 | grep -v node_modules

Repository: usebruno/bruno

Length of output: 7677


🏁 Script executed:

# Check CodeMirror API documentation - specifically look for undo/redo methods
rg "editor\.undo\(|editor\.redo\(" packages/bruno-app/src --type js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Look for how QueryEditor uses execCommand
cat packages/bruno-app/src/components/RequestPane/QueryEditor/index.js | grep -A 10 -B 5 "execCommand"

Repository: usebruno/bruno

Length of output: 480


🏁 Script executed:

# Check menu-template.js in electron to see if there's a proper implementation
cat packages/bruno-electron/src/app/menu-template.js | head -100

Repository: usebruno/bruno

Length of output: 2308


🏁 Script executed:

# Search for any way the AppMenu gets access to the active editor instance
rg "activeElement|querySelector.*editor|getActiveEditor\|currentEditor" packages/bruno-app/src/components/AppTitleBar --type js

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check if there's state or Redux connection for the focused editor
rg "activeEditor|focusedEditor\|selectedTab" packages/bruno-app/src --type js -B 2 -A 2 | head -50

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Verify AppMenu is a React component menu, not Electron native menu
cat packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js | head -50

Repository: usebruno/bruno

Length of output: 1385


🌐 Web query:

CodeMirror undo redo API execCommand vs cm.undo

💡 Result:

CodeMirror 5 (the “classic” CodeMirror)

You have two ways to trigger undo/redo:

  • Direct history methods: cm.undo() / cm.redo() (and cm.clearHistory(), etc.). These directly invoke the editor’s history behavior. [2]
  • Command dispatcher: cm.execCommand("undo") / cm.execCommand("redo"). This looks up the named command in CodeMirror.commands and runs it on the editor instance. [1] The built-in command list includes undo/redo. [3]

Practical difference (CM5):

  • If you want to “do the same thing the keybinding would do”, prefer execCommand, because it goes through the command layer (so it respects any overrides/custom commands/addons that replace CodeMirror.commands.undo). [1][3]
  • If you just want the editor to undo regardless of command indirection, cm.undo() is the direct API. [2]

CodeMirror 6 (the current major version)

There is no cm.undo() method and no execCommand() on the view. Undo/redo are commands (undo, redo, etc.) implemented by the history system, and you run them as functions (e.g., undo(view)), with history provided via the history functionality. [4]

Key point: in CM6, undo/redo only work if the editor has history enabled/configured (via the history-related setup), whereas CM5 always had built-in history methods. [4]

Sources:
[1] CodeMirror manual: cm.execCommand(name) runs a named command. (codemirror.net)
[2] CodeMirror manual: undo() / redo() methods exist on the editor instance. (draytek.com)
[3] CodeMirror manual: built-in commands include undo / redo. (codemirror.net)
[4] CodeMirror (v6) reference: undo / redo are StateCommands (run as commands/functions). (codemirror.net)

Citations:


🏁 Script executed:

# Check if AppMenu edit items are actually wired to do anything or if they're just UI
rg "editItems|AppMenu" packages/bruno-app/src/components --type js -B 1 -A 1 | head -40

Repository: usebruno/bruno

Length of output: 1749


🏁 Script executed:

# Verify if the Electron menu-template.js is the actual handler for these shortcuts
rg "undo|redo" packages/bruno-electron/src --type js -B 2 -A 2

Repository: usebruno/bruno

Length of output: 485


document.execCommand won't work with CodeMirror editors in Bruno.

While execCommand is deprecated, the real issue is functional: it only works on contentEditable elements or the document. Bruno's editors (CodeMirror v5) have their own undo/redo/copy/paste APIs (cm.undo(), cm.redo(), etc.) that are separate from the DOM.

These menu items will likely fail silently or not affect the focused editor. Consider delegating to editor instances or using IPC handlers like the File menu does (similar to the native Electron menu-template.js approach).

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js` around lines
74 - 113, The editItems menu currently uses deprecated document.execCommand (see
editItems and the entries with id
'undo','redo','cut','copy','paste','select-all'), which won't work for Bruno's
CodeMirror editors; update each onClick to delegate to the active editor
instance (e.g. find the focused CodeMirror v5 instance and call its APIs like
cm.undo(), cm.redo(), cm.cut/replaceSelection or use clipboard/selection APIs
for copy/paste, and cm.execCommand equivalents) or route the action through the
same IPC handler pattern used by the File menu / menu-template.js so the main
process or editor context performs the operation on the correct editor instead
of calling document.execCommand.


return (
<StyledWrapper>
<MenuDropdown
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we update the menuDropdown component to take items (submenu) as array?
Note: Make sure this is not adding any jsx render load on the existing menu dropdown which doesn't have submenu items

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-app/src/ui/MenuDropdown/index.js (1)

240-251: Keyboard users can’t open submenus.
Action keys always call handleItemClick, which closes the menu when the focused item has a submenu. Consider opening the submenu on Enter/Space (and ArrowRight) and closing it on ArrowLeft/Escape so nested items are reachable from the keyboard.

🧹 Nitpick comments (1)
packages/bruno-app/src/components/AppTitleBar/AppMenu/index.js (1)

9-33: Avoid ipcRenderer existence checks in renderer code.
Optional chaining here can mask wiring issues; this codebase assumes ipcRenderer is available, so call it directly (mock in tests if needed). Apply the same change to the remaining ipcRenderer calls in this file.

♻️ Suggested update (apply across ipcRenderer calls)
-          onClick: () => ipcRenderer?.invoke('renderer:open-collection')
+          onClick: () => ipcRenderer.invoke('renderer:open-collection')
-          onClick: () => ipcRenderer?.invoke('renderer:open-preferences')
+          onClick: () => ipcRenderer.invoke('renderer:open-preferences')

Based on learnings: In the Bruno Electron renderer code (packages/bruno-app), assume window.ipcRenderer is always available and skip existence checks. Do not guard for ipcRenderer in this Electron context; use window.ipcRenderer directly (e.g., window.ipcRenderer.send(...), window.ipcRenderer.on(...)).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-app/src/ui/MenuDropdown/index.js (1)

227-263: Missing keyboard navigation for submenus (accessibility).

The keyboard handler supports vertical navigation but doesn't implement ArrowRight to open a focused submenu or ArrowLeft to close it and return to the parent. This is the standard ARIA menu pattern for nested menus and improves keyboard accessibility.

Consider adding:

  • ArrowRight on a submenu trigger → open submenu and focus first item
  • ArrowLeft inside a submenu → close submenu and return focus to parent trigger
🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/ui/MenuDropdown/index.js`:
- Around line 389-442: SubmenuItem is currently declared inside MenuDropdown
causing hooks to be recreated each render; extract SubmenuItem as a top-level
component and pass in all external values it needs (e.g., item, onRootClose,
submenuPlacement, getMenuItemProps, renderMenuItemContent, and any icons like
IconChevronLeft/IconChevronRight or a computed ArrowIcon prop); update the
MenuDropdown usage to render <SubmenuItem ... /> with the computed
submenuItemsWithClose and submenuTippyPlacement (or compute those outside and
pass them in) so SubmenuItem can keep its internal submenuOpen state stable
across renders.
🧹 Nitpick comments (1)
packages/bruno-app/src/ui/MenuDropdown/index.js (1)

271-277: Click-outside check is overly broad.

The check event?.target?.closest?.('[data-tippy-root]') prevents closing when clicking inside any tippy element on the page, not just this dropdown's submenus. If other tippy-based components exist simultaneously, this could prevent expected close behavior.

Consider scoping to the specific submenu hierarchy if this becomes problematic in practice.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/bruno-app/src/ui/MenuDropdown/index.js (1)

489-491: Signature mismatch: handleClickOutside expects (instance, event) but onClick passes (event).

At line 490, handleClickOutside is used as an onClick handler. This works by accident due to optional chaining, but semantically instance receives the event object while event is undefined. Consider using an inline handler or a dedicated function for clarity.

🔧 Suggested fix
-        <div className="dropdown-header-container" onClick={handleClickOutside}>
+        <div className="dropdown-header-container" onClick={() => updateOpenState(false)}>
🤖 Fix all issues with AI agents
In `@packages/bruno-app/src/ui/MenuDropdown/SubMenuItem/index.js`:
- Around line 41-62: The submenu currently only uses onMouseEnter/onMouseLeave
so keyboard users cannot open it; add a keyboard handler (onKeyDown) to the
submenu wrapper (the div with className "submenu-trigger" or the inner div that
spreads itemProps) to call setSubmenuOpen(true) when ArrowRight (and Enter/Space
if desired) is pressed, and call setSubmenuOpen(false) on ArrowLeft or Escape;
when opening via keyboard, move focus into the first focusable element inside
MenuDropdownComponent (use a ref to MenuDropdownComponent or the rendered
submenu items array submenuItemsWithClose to focus the first item) and ensure
event.preventDefault()/stopPropagation() as appropriate to avoid interfering
with parent handlers, and keep showTickMark/submenuPlacement behavior unchanged.
🧹 Nitpick comments (2)
packages/bruno-app/src/ui/MenuDropdown/SubMenuItem/index.js (1)

17-26: Consider memoizing submenuItemsWithClose to avoid unnecessary re-renders.

This array is recreated on every render, which means MenuDropdownComponent receives a new items reference each time and may re-render unnecessarily.

♻️ Suggested fix using useMemo
 import React, { useState } from 'react';
+import React, { useState, useMemo } from 'react';
-  const submenuItemsWithClose = item.submenu.map((subItem) => {
-    if (subItem.type === 'divider') return subItem;
-    return {
-      ...subItem,
-      onClick: () => {
-        subItem.onClick?.();
-        onRootClose();
-      }
-    };
-  });
+  const submenuItemsWithClose = useMemo(() => item.submenu.map((subItem) => {
+    if (subItem.type === 'divider') return subItem;
+    return {
+      ...subItem,
+      onClick: () => {
+        subItem.onClick?.();
+        onRootClose();
+      }
+    };
+  }), [item.submenu, onRootClose]);
packages/bruno-app/src/ui/MenuDropdown/index.js (1)

357-387: Consider memoizing getMenuItemProps and renderMenuItemContent with useCallback.

These functions are passed as props to SubMenuItem. Without memoization, SubMenuItem receives new function references on every parent render, which can trigger unnecessary child re-renders. Per project guidelines, memoize when necessary—given these are passed down, it's worth considering.

♻️ Optional memoization
-  const getMenuItemProps = (item, extraProps = {}) => {
+  const getMenuItemProps = useCallback((item, extraProps = {}) => {
     const selectIndentClass = item.groupStyle === 'select' ? 'dropdown-item-select' : '';
     const isActive = item.id === selectedItemId;
     // ... rest unchanged
     return { ... };
-  };
+  }, [selectedItemId, testId]);

-  const renderMenuItemContent = (item, rightContent = null) => (
+  const renderMenuItemContent = useCallback((item, rightContent = null) => (
     <>
       {renderSection(item.leftSection)}
       <span className="dropdown-label">{item.label}</span>
       {rightContent}
     </>
-  );
+  ), []);

Comment on lines +41 to +62
return (
<div
className="submenu-trigger"
onMouseEnter={() => setSubmenuOpen(true)}
onMouseLeave={() => setSubmenuOpen(false)}
>
<MenuDropdownComponent
items={submenuItemsWithClose}
placement={submenuTippyPlacement}
opened={submenuOpen}
onChange={setSubmenuOpen}
showTickMark={false}
submenuPlacement={submenuPlacement}
appendTo={() => document.body}
offset={[0, 0]}
>
<div {...itemProps}>
{renderMenuItemContent(item, arrowElement)}
</div>
</MenuDropdownComponent>
</div>
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Accessibility: Submenus lack keyboard navigation support.

Currently, submenus only open/close via mouse hover events (onMouseEnter/onMouseLeave). Users navigating via keyboard have no way to open or navigate into submenus. Consider adding ArrowRight to open and ArrowLeft/Escape to close the submenu for better a11y compliance.

🤖 Prompt for AI Agents
In `@packages/bruno-app/src/ui/MenuDropdown/SubMenuItem/index.js` around lines 41
- 62, The submenu currently only uses onMouseEnter/onMouseLeave so keyboard
users cannot open it; add a keyboard handler (onKeyDown) to the submenu wrapper
(the div with className "submenu-trigger" or the inner div that spreads
itemProps) to call setSubmenuOpen(true) when ArrowRight (and Enter/Space if
desired) is pressed, and call setSubmenuOpen(false) on ArrowLeft or Escape; when
opening via keyboard, move focus into the first focusable element inside
MenuDropdownComponent (use a ref to MenuDropdownComponent or the rendered
submenu items array submenuItemsWithClose to focus the first item) and ensure
event.preventDefault()/stopPropagation() as appropriate to avoid interfering
with parent handlers, and keep showTickMark/submenuPlacement behavior unchanged.

@bijin-bruno bijin-bruno merged commit ba16656 into usebruno:main Jan 30, 2026
7 of 8 checks passed
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.

3 participants