Agentic UI: add a native text context menu - #4388
Conversation
…ok up Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a native, OS-level text context menu for the Agentic UI in Electron by routing right-click context from the renderer to the host (main process) and building an Electron Menu template that conditionally includes Look Up (macOS only), Copy, Copy All, and Paste.
Changes:
- Add a renderer hook (
useTextContextMenu) that capturescontextmenuevents, detects selection/editability/message text, and calls an optional connector method. - Add a main-process menu builder (
buildTextContextMenuTemplate) + IPC handler (showTextContextMenu) to pop a native menu and perform actions (Look Up, Copy All, Paste). - Wire the IPC connector + preload + handler registration, and add unit tests for both renderer and main-process menu-template behavior.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| apps/ui/src/ui-classic/components/session-view/conversation/index.tsx | Adds data-message-text attributes to message containers so “Copy All” can target the full message. |
| apps/ui/src/ui-classic/app.tsx | Installs useTextContextMenu() at app root so right-clicks are handled globally. |
| apps/ui/src/hooks/use-text-context-menu.ts | New hook that gathers selection/editability/message context and requests the host-native context menu. |
| apps/ui/src/hooks/use-text-context-menu.test.tsx | Tests hook behavior: message targeting, ignoring unrelated UI, selection intersection, editable detection. |
| apps/ui/src/data/core/types.ts | Extends Connector with optional showTextContextMenu API (absent in browser builds). |
| apps/ui/src/data/core/connectors/ipc/index.ts | Implements showTextContextMenu via ipcApi.showTextContextMenu(...). |
| apps/studio/src/text-context-menu.ts | Implements menu-template construction and the Electron-side popup handler (Look Up, Copy, Copy All, Paste). |
| apps/studio/src/tests/text-context-menu.test.ts | Tests menu-template composition, platform gating, truncation, and section/separator logic. |
| apps/studio/src/preload.ts | Exposes ipcApi.showTextContextMenu using ipcRenderer.send (void handler). |
| apps/studio/src/ipc-handlers.ts | Exports showTextContextMenu from the main-process handler module for IPC registration. |
| apps/studio/src/constants.ts | Registers showTextContextMenu as an IPC void handler. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function AssistantText( { text, copyText }: { text: string; copyText?: string } ) { | ||
| return ( | ||
| <div className={ styles.assistantTurn }> | ||
| // The attribute carries the whole message so a right-click anywhere | ||
| // inside it can offer Copy All, not just the selection. | ||
| <div className={ styles.assistantTurn } { ...{ [ MESSAGE_TEXT_ATTRIBUTE ]: copyText ?? text } }> |
📊 Performance Test ResultsComparing 7d78ab7 vs trunk app-size
site-editor
site-startup
Results are median values from multiple test runs. Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff) |
bcotrim
left a comment
There was a problem hiding this comment.
Looks good!
Some comments:
- On the composer only "Paste" is available, should we also offer "Copy"?
-
"Copy All" is a translated string. Should the other options also be translated?
menu.ts:364-366already does{ label: __( 'Copy' ), role: 'copy' }— macOS
localizes roles natively, so this would only show on Windows/Linux. -
conversation/index.tsx:473usescopyText ?? text, butcopyTextis only set on
the last text block (line 281). On a reply split by tool calls, right-clicking an
earlier paragraph makes "Copy All" copy just that fragment — the comment on line 262 says it should yield the whole message.
Suggestion: what do you think about an option to quote the selected text into the
composer? Could be useful for referencing part of the agent's reply. Could be done in a follow-up PR if you think it's a good idea
Related issues
How AI was used in this PR
AI wrote the implementation and tests from a scope I set interactively — we went back and forth on what the menu should contain and how the message text should reach the main process. I reviewed the approach and tested it in the app.
Proposed Changes
Right-click did nothing anywhere in Studio. No Copy, no Look Up, no Paste in the composer — the app just felt inert in a way desktop apps aren't.
That's because Electron ships no default context menu. The menu Chrome shows is built by Chrome's own browser layer, which isn't part of the content layer Electron embeds, so every Electron app has to declare its own. Studio never did.
This adds one. It's a real native menu on every platform — NSMenu on macOS, a Win32 popup on Windows, GTK on Linux — with the same appearance, keyboard handling and accessibility the OS gives any other app. Only the item list is ours.
What you get:
Look Up is the one that only makes sense on a Mac — it opens the system Dictionary panel for the selected word. Windows and Linux expose no system dictionary to apps; their native text menus really are just the edit commands, so gating it on platform gives each OS what it would natively show rather than a lowest-common-denominator menu.
Copy All copies the whole message rather than just the highlighted part, which is usually what you actually want in a transcript. It works on your own prompts as well as the agent's replies — it would be strange for right-click to work on one bubble and not the other.
Every item is conditional on whether it would actually do something, so nothing appears greyed out: no dead Paste in the read-only transcript, no Copy without a selection. If nothing applies, no menu opens at all rather than an empty one.
Deliberately not included: undo, redo, cut and delete. The transcript is read-only and the composer is a plain field where they'd be noise.
The browser builds are untouched — they already have a real context menu, so the connector method is simply absent there and the right-click falls through to Chrome's own.
Testing Instructions
This is a main-process change, so it needs a full app restart —
rs/ hot reload won't pick it up.Look Up "…", a divider,Copy,Copy All.Copy,Copy All.Copy All.Copy Allworks there too.Pasteappears. Clear the clipboard and it doesn't.Unit tests:
npm test -- apps/studio/src/tests/text-context-menu.test.tsPre-merge Checklist
Notes for reviewers:
apps/studioandapps/uitypecheck clean; eslint clean; 9 new unit tests plus the fullapps/uisuite (66 files, 422 tests) pass.npm run typecheckcurrently fails inapps/clion@earendil-works/pi-ai— that's pre-existing on trunk (stalenode_modulesafter the recentpackages/commonbump), confirmed by stashing this branch's changes.npm installclears it.conversation/index.tsx, which Agentic UI: fix copy button space in chat #4334 also changes. Expect a one-line conflict inAssistantTextfor whichever merges second.