feat: add custom AppMenu component for windows & linux#6934
feat: add custom AppMenu component for windows & linux#6934bijin-bruno merged 5 commits intousebruno:mainfrom
Conversation
WalkthroughAdds 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 onipcRendererin renderer code.
The repo convention is to treatwindow.ipcRendereras always available inpackages/bruno-appand avoid guards. Based on learnings, align these calls accordingly.Based on learnings, align renderer IPC usage to unguarded calls.♻️ 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')
| 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> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's find and examine the AppMenu file
find . -type f -name "index.js" | grep -i appmenu | head -5Repository: 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 -5Repository: 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/nullRepository: 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 -80Repository: 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 -5Repository: usebruno/bruno
Length of output: 255
🏁 Script executed:
# Examine the MenuDropdown component
cat -n packages/bruno-app/src/ui/MenuDropdown/index.js | head -150Repository: 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.jsRepository: 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 -5Repository: usebruno/bruno
Length of output: 100
🏁 Script executed:
# Check the Dropdown component implementation
cat -n packages/bruno-app/src/components/Dropdown/index.js | head -150Repository: usebruno/bruno
Length of output: 1179
🏁 Script executed:
# Check if Dropdown is a Tippy wrapper
wc -l packages/bruno-app/src/components/Dropdown/index.jsRepository: 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.jsRepository: 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.jsRepository: 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.jsRepository: 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.
| return ( | ||
| <StyledWrapper ref={menuRef}> | ||
| <MenuDropdown |
There was a problem hiding this comment.
.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.
| 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.
There was a problem hiding this comment.
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.
| 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') | ||
| } | ||
| ]; |
There was a problem hiding this comment.
🧩 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 thanexecCommand('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.jsondepends oncodemirror(e.g.,codemirror: 5.65.2). [2]- Bruno’s editor components instantiate CodeMirror directly (e.g.,
const CodeMirror = require('codemirror');andCodeMirror(...)). [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:
- 1: https://fossies.org/linux/bruno/packages/bruno-electron/package.json?utm_source=openai
- 2: https://fossies.org/linux/bruno/packages/bruno-app/package.json?utm_source=openai
- 3: https://fossies.org/linux/bruno/packages/bruno-app/src/components/MultiLineEditor/index.js?utm_source=openai
🏁 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 -20Repository: 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=10Repository: 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 -120Repository: 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 -80Repository: 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 jsRepository: 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 -10Repository: 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 -60Repository: 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 -iRepository: 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 -20Repository: 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=15Repository: 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 -100Repository: 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 2Repository: 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 -40Repository: 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/nullRepository: 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 jsRepository: 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 -10Repository: 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/nullRepository: 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 -150Repository: 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 2Repository: 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 -10Repository: 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 jsRepository: 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_modulesRepository: 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 jsRepository: 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 -100Repository: 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 jsRepository: 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 -50Repository: 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 -50Repository: 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()(andcm.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 inCodeMirror.commandsand runs it on the editor instance. [1] The built-in command list includesundo/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 replaceCodeMirror.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:
- 1: https://codemirror.net/3/doc/manual.html
- 2: https://www.draytek.com/assets/components/codemirror/cm/doc/manual.html
- 3: https://codemirror.net/3/doc/manual.html
- 4: https://codemirror.net/docs/ref/?utm_source=openai
🏁 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 -40Repository: 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 2Repository: 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 callhandleItemClick, which closes the menu when the focused item has asubmenu. 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(...)).
There was a problem hiding this comment.
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
ArrowRightto open a focused submenu orArrowLeftto close it and return to the parent. This is the standard ARIA menu pattern for nested menus and improves keyboard accessibility.Consider adding:
ArrowRighton a submenu trigger → open submenu and focus first itemArrowLeftinside 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.
There was a problem hiding this comment.
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:handleClickOutsideexpects(instance, event)butonClickpasses(event).At line 490,
handleClickOutsideis used as anonClickhandler. This works by accident due to optional chaining, but semanticallyinstancereceives the event object whileeventis 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 memoizingsubmenuItemsWithCloseto avoid unnecessary re-renders.This array is recreated on every render, which means
MenuDropdownComponentreceives a newitemsreference 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 memoizinggetMenuItemPropsandrenderMenuItemContentwith useCallback.These functions are passed as props to
SubMenuItem. Without memoization,SubMenuItemreceives 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} </> - ); + ), []);
| 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> | ||
| ); |
There was a problem hiding this comment.
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.
Description
Recording.2026-01-27.122358.mp4
Contribution Checklist:
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
Style
✏️ Tip: You can customize this high-level summary in your review settings.