Skip to content

feat: add right-click context menu to messages#706

Open
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/message-context-menu
Open

feat: add right-click context menu to messages#706
hankkyy wants to merge 1 commit into
fathah:mainfrom
hankkyy:feat/message-context-menu

Conversation

@hankkyy

@hankkyy hankkyy commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Feature

Add right-click context menu to message bubbles. Right-clicking any chat message shows a context menu with a 'Copy message' action that copies the full message content to clipboard.

Changes

  • MessageRow: onContextMenu handler, context menu state (position x/y), fixed-position menu rendered at cursor, click-outside and Escape to dismiss, handleCopy via window.hermesAPI.copyToClipboard
  • main.css: .chat-context-menu (fixed, elevated, rounded with shadow) + .chat-context-menu-item (flex row, hover highlight)
  • i18n: copyMessage added to all 10 locales

Right-clicking a message bubble now shows a context menu with a
'Copy message' action that copies the full content to clipboard.

Changes:
- MessageRow: add onContextMenu handler, context menu state,
  fixed-position menu with click-outside/Escape dismissal
- main.css: .chat-context-menu (fixed, elevated with shadow) +
  .chat-context-menu-item (flex row with hover)
- i18n: add copyMessage to all 10 locales
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a right-click context menu to chat message bubbles with a single "Copy message" action that calls window.hermesAPI.copyToClipboard. The feature covers all 10 supported locales and introduces dedicated CSS classes using existing design tokens.

  • MessageRow.tsx: Adds per-row contextMenu state, a handleContextMenu handler attached to the outer bubble div, a useEffect that dismisses the menu on click-outside or Escape, and a handleCopy callback that writes bubbleContent to the clipboard via the preload API.
  • main.css: Two new classes (.chat-context-menu, .chat-context-menu-item) style the floating menu using existing CSS variables for colors, radius, and shadow.
  • i18n files: copyMessage key added consistently to all 10 locale files.

Confidence Score: 3/5

The copy action and CSS are solid, but the dismiss logic has two interaction bugs that will be visible to users in normal usage.

Each MessageRow owns its own context menu state, and the dismiss effect only listens for click events — not contextmenu events. Right-clicking a second message while one menu is already open fires a contextmenu event (not a click), so the first menu's onClick listener never fires, and both menus end up visible at the same time. Separately, because the menu div sits inside the outer div that carries onContextMenu={handleContextMenu}, right-clicking on the open menu itself re-triggers the handler via bubbling and repositions the menu unexpectedly. Both issues reproduce easily in a normal chat interaction.

src/renderer/src/screens/Chat/MessageRow.tsx — the context-menu dismiss and propagation logic needs attention before merge.

Important Files Changed

Filename Overview
src/renderer/src/screens/Chat/MessageRow.tsx Adds context menu state, dismiss effect, and copy handler. Two logic bugs: the dismiss effect does not listen for contextmenu events so multiple menus can stack, and right-clicking the open menu re-positions it via bubbling.
src/renderer/src/assets/main.css Adds two new CSS classes for the context menu; uses existing design tokens; no issues found.
src/shared/i18n/locales/en/chat.ts Adds copyMessage key; translation is correct and consistently placed across all 10 locales.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant MessageRow
    participant Document
    participant hermesAPI

    User->>MessageRow: right-click (contextmenu event)
    MessageRow->>MessageRow: "handleContextMenu → setContextMenu({x, y})"
    MessageRow->>Document: addEventListener("click", onClick, true)
    MessageRow->>Document: addEventListener("keydown", onKey)
    MessageRow-->>User: render .chat-context-menu at cursor

    alt User clicks "Copy message"
        User->>MessageRow: click handleCopy
        MessageRow->>hermesAPI: copyToClipboard(bubbleContent)
        hermesAPI-->>MessageRow: resolved
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    else User clicks outside menu
        User->>Document: click (captured)
        Document->>MessageRow: onClick fires (target not in menuRef)
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    else User presses Escape
        User->>Document: keydown Escape
        Document->>MessageRow: onKey fires
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant MessageRow
    participant Document
    participant hermesAPI

    User->>MessageRow: right-click (contextmenu event)
    MessageRow->>MessageRow: "handleContextMenu → setContextMenu({x, y})"
    MessageRow->>Document: addEventListener("click", onClick, true)
    MessageRow->>Document: addEventListener("keydown", onKey)
    MessageRow-->>User: render .chat-context-menu at cursor

    alt User clicks "Copy message"
        User->>MessageRow: click handleCopy
        MessageRow->>hermesAPI: copyToClipboard(bubbleContent)
        hermesAPI-->>MessageRow: resolved
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    else User clicks outside menu
        User->>Document: click (captured)
        Document->>MessageRow: onClick fires (target not in menuRef)
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    else User presses Escape
        User->>Document: keydown Escape
        Document->>MessageRow: onKey fires
        MessageRow->>MessageRow: closeContextMenu → setContextMenu(null)
        MessageRow->>Document: removeEventListener (cleanup)
    end
Loading

Reviews (1): Last reviewed commit: "feat: add right-click context menu to me..." | Re-trigger Greptile

Comment on lines +96 to +113
// Close context menu on click outside or Escape
useEffect(() => {
if (!contextMenu) return;
function onClick(e: MouseEvent): void {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
closeContextMenu();
}
}
function onKey(e: KeyboardEvent): void {
if (e.key === "Escape") closeContextMenu();
}
document.addEventListener("click", onClick, true);
document.addEventListener("keydown", onKey);
return () => {
document.removeEventListener("click", onClick, true);
document.removeEventListener("keydown", onKey);
};
}, [contextMenu, closeContextMenu]);

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 Multiple context menus can stack simultaneously

The dismiss effect only listens for click events, so a right-click (contextmenu event) on a different message row does not fire onClick and therefore never closes the current menu. Because every MessageRow manages its own state independently, the second right-click opens a second chat-context-menu alongside the first. The user ends up with two (or more) stacked menus that cannot be closed without an extra regular click.

The fix is to also listen for contextmenu on the document and close any open menu when it fires (before the targeted row's handleContextMenu re-opens one for the new position).

Comment on lines +194 to +199
{contextMenu && (
<div
ref={menuRef}
className="chat-context-menu"
style={{ left: contextMenu.x, top: contextMenu.y }}
>

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 Right-clicking directly on the open context menu bubbles the contextmenu event up to the outer div's onContextMenu={handleContextMenu}, which calls setContextMenu with new coordinates and re-positions the menu. Adding onContextMenu={e => e.stopPropagation()} on the menu container prevents this.

Suggested change
{contextMenu && (
<div
ref={menuRef}
className="chat-context-menu"
style={{ left: contextMenu.x, top: contextMenu.y }}
>
{contextMenu && (
<div
ref={menuRef}
className="chat-context-menu"
style={{ left: contextMenu.x, top: contextMenu.y }}
onContextMenu={(e) => e.stopPropagation()}
>

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

<div
ref={menuRef}
className="chat-context-menu"
style={{ left: contextMenu.x, top: contextMenu.y }}

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.

P2 Menu can render off-screen near viewport edges

contextMenu.x and contextMenu.y are raw cursor coordinates. When the cursor is in the lower-right corner of the viewport the 160 px-wide, ~40 px-tall menu overflows outside the window with no way to scroll it back into view. A boundary check clamping left to Math.min(x, window.innerWidth - menuWidth) and top to Math.min(y, window.innerHeight - menuHeight) would keep it fully visible.

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.

1 participant