Skip to content

Conversation

@pulkitbajaj
Copy link
Contributor

@pulkitbajaj pulkitbajaj commented Dec 25, 2025

User description

Closes #14655

This PR implements a context menu for the AI chat components, enabling users to copy specific text selections or the entire message via a right-click. Previously, users had no way to interact with the chat text using a mouse.

Steps to test

  1. Open the AI Chat window.
  2. Select a few words of an AI or User message.
  3. Right-click the selection and choose "Copy."
  4. Paste into a text editor to verify only the selected text was copied.
  5. Right-click a message without any selection and choose "Copy."
  6. Paste to verify the entire message was copied.

Mandatory checks

image

PR Type

Enhancement


Description

  • Adds right-click context menu to AI chat messages

  • Enables copying selected text or entire message

  • Supports UserMessage, AiMessage, and ErrorMessage types

  • Integrates with system clipboard for paste functionality


Diagram Walkthrough

flowchart LR
  User["User right-clicks message"]
  Detect["Detect selection via event filter"]
  Menu["Display context menu"]
  Copy["Copy action triggered"]
  Clipboard["Write to system clipboard"]
  User --> Detect
  Detect --> Menu
  Menu --> Copy
  Copy --> Clipboard
Loading

File Walkthrough

Relevant files
Enhancement
ChatMessageComponent.java
Implement context menu with copy functionality                     

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java

  • Added imports for ContextMenu, MenuItem, Clipboard, and
    ClipboardContent
  • Implemented setupContextMenu() method to create context menu with copy
    functionality
  • Added event filter on markdownContentPane to capture text selection on
    right-click
  • Supports copying selected text or entire message based on selection
    state
  • Handles multiple message types (UserMessage, AiMessage, ErrorMessage)
+66/-0   

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Dec 25, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🟢
No security concerns identified No security vulnerabilities detected by AI analysis. Human verification advised for critical code.
Ticket Compliance
🟡
🎫 #13109
🔴 Add Pseudonymization functionality to the CLI
Make org.jabref.logic.pseudonymization.Pseudonymization available on the CLI
Provide similar CLI experience to the consistency check
Follow the implementation pattern of org.jabref.cli.CheckConsistency class
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link
Contributor

qodo-free-for-open-source-projects bot commented Dec 25, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
Possible issue
Capture selection state more reliably
Suggestion Impact:The commit implements the core idea of the suggestion by moving selection capture logic to the context menu event handler (lines 76-96). However, instead of using setOnContextMenuRequested, it keeps the MOUSE_PRESSED event filter approach. The commit also refactors the implementation significantly: it stores selection in a field (selectedText) instead of contextMenu properties, extracts text immediately rather than storing indices, uses ClipBoardManager instead of direct Clipboard API, and adds ChatMessageUtils for content extraction. While the exact API suggested (setOnContextMenuRequested) was not used, the intention to improve selection capture reliability was addressed through the refactored approach.

code diff:

@@ -115,9 +94,17 @@
                 try {
                     int start = markdownTextFlow.getSelectionStartIndex();
                     int end = markdownTextFlow.getSelectionEndIndex();
-                    contextMenu.getProperties().put("selection", new int[] {start, end});
-                } catch (AssertionError | Exception e) {
-                    contextMenu.getProperties().put("selection", new int[] {-1, -1});
+
+                    String fullText = ChatMessageUtils.getContent(chatMessage.get()).orElse("");
+
+                    if (start >= 0 && end > start && !fullText.isEmpty()) {
+                        this.selectedText = fullText.substring(start, Math.min(end, fullText.length()));
+                    } else {
+                        this.selectedText = "";
+                    }
+                } catch (AssertionError | IndexOutOfBoundsException e) {
+                    LOGGER.debug("Failed to extract selection indices for copy action", e);
+                    this.selectedText = "";
                 }
             }
         });

Use the setOnContextMenuRequested event instead of MOUSE_PRESSED to reliably
capture the final text selection state before showing the context menu.

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java [113-123]

-markdownContentPane.addEventFilter(MOUSE_PRESSED, event -> {
-    if (event.isSecondaryButtonDown()) {
-        try {
-            int start = markdownTextFlow.getSelectionStartIndex();
-            int end = markdownTextFlow.getSelectionEndIndex();
-            contextMenu.getProperties().put("selection", new int[] {start, end});
-        } catch (AssertionError | Exception e) {
-            contextMenu.getProperties().put("selection", new int[] {-1, -1});
-        }
+markdownContentPane.setOnContextMenuRequested(event -> {
+    if (markdownTextFlow.isSelectionActive()) {
+        int start = markdownTextFlow.getSelectionStartIndex();
+        int end = markdownTextFlow.getSelectionEndIndex();
+        contextMenu.getProperties().put("selection", new int[]{start, end});
+    } else {
+        contextMenu.getProperties().put("selection", new int[]{-1, -1});
     }
+    contextMenu.show(markdownContentPane, event.getScreenX(), event.getScreenY());
 });

[Suggestion processed]

Suggestion importance[1-10]: 8

__

Why: This suggestion correctly identifies a behavioral bug where using MOUSE_PRESSED can capture an incomplete text selection. Moving the logic to setOnContextMenuRequested is the correct approach for this use case, making the feature more reliable and robust, while also simplifying the code.

Medium
General
Refactor clipboard logic and add validation
Suggestion Impact:The commit addresses the same clipboard logic area but takes a different approach. Instead of creating a separate copyToClipboard helper method as suggested, it refactors the selection handling by storing selectedText as a field and extracting message content to a utility method (ChatMessageUtils.getContent). The boundary check suggestion (start < fullText.length()) is implemented in line 62, preventing the IndexOutOfBoundsException. While the implementation differs from the exact suggestion, it achieves the same goals of improving code organization and adding validation.

code diff:

         copyItem.setOnAction(_ -> {
-            int[] selection = (int[]) contextMenu.getProperties().getOrDefault("selection", new int[] {-1, -1});
-            int start = selection[0];
-            int end = selection[1];
+            String textToCopy = !this.selectedText.isEmpty()
+                                ? this.selectedText
+                                : ChatMessageUtils.getContent(chatMessage.get()).orElse("");
 
-            String fullText = switch (chatMessage.get()) {
-                case UserMessage user ->
-                        user.singleText();
-                case AiMessage ai ->
-                        ai.text();
-                case ErrorMessage err ->
-                        err.getText();
-                case null,
-                     default ->
-                        "";
-            };
-
-            if (fullText.isEmpty()) {
+            if (textToCopy.isEmpty()) {
                 return;
-            }
-
-            String textToCopy;
-            if (start >= 0 && end > start) {
-                int safeEnd = Math.min(end, fullText.length());
-                textToCopy = fullText.substring(start, safeEnd);
-            } else {
-                textToCopy = fullText;
             }
 
             final Clipboard clipboard = Clipboard.getSystemClipboard();
@@ -115,9 +97,16 @@
                 try {
                     int start = markdownTextFlow.getSelectionStartIndex();
                     int end = markdownTextFlow.getSelectionEndIndex();
-                    contextMenu.getProperties().put("selection", new int[] {start, end});
+
+                    String fullText = ChatMessageUtils.getContent(chatMessage.get()).orElse("");
+
+                    if (start >= 0 && end > start && !fullText.isEmpty()) {
+                        this.selectedText = fullText.substring(start, Math.min(end, fullText.length()));
+                    } else {
+                        this.selectedText = "";
+                    }
                 } catch (AssertionError | Exception e) {
-                    contextMenu.getProperties().put("selection", new int[] {-1, -1});
+                    this.selectedText = "";
                 }

Refactor the clipboard logic into a separate helper method and add a boundary
check for the selection's start index to prevent potential runtime errors.

jabgui/src/main/java/org/jabref/gui/ai/components/aichat/chatmessage/ChatMessageComponent.java [76-109]

+private void copyToClipboard(String text) {
+    if (text == null || text.isEmpty()) {
+        return;
+    }
+    final Clipboard clipboard = Clipboard.getSystemClipboard();
+    final ClipboardContent content = new ClipboardContent();
+    content.putString(text);
+    clipboard.setContent(content);
+}
+
+...
+
 copyItem.setOnAction(_ -> {
     int[] selection = (int[]) contextMenu.getProperties().getOrDefault("selection", new int[] {-1, -1});
     int start = selection[0];
     int end = selection[1];
 
     String fullText = switch (chatMessage.get()) {
-        case UserMessage user ->
-                user.singleText();
-        case AiMessage ai ->
-                ai.text();
-        case ErrorMessage err ->
-                err.getText();
-        case null,
-             default ->
-                "";
+        case UserMessage user -> user.singleText();
+        case AiMessage ai -> ai.text();
+        case ErrorMessage err -> err.getText();
+        case null, default -> "";
     };
 
     if (fullText.isEmpty()) {
         return;
     }
 
     String textToCopy;
-    if (start >= 0 && end > start) {
+    if (start >= 0 && end > start && start < fullText.length()) {
         int safeEnd = Math.min(end, fullText.length());
         textToCopy = fullText.substring(start, safeEnd);
     } else {
         textToCopy = fullText;
     }
 
-    final Clipboard clipboard = Clipboard.getSystemClipboard();
-    final ClipboardContent content = new ClipboardContent();
-    content.putString(textToCopy);
-    clipboard.setContent(content);
+    copyToClipboard(textToCopy);
 });

[To ensure code accuracy, apply this suggestion manually]

Suggestion importance[1-10]: 7

__

Why: The suggestion correctly identifies a potential IndexOutOfBoundsException if start is not less than fullText.length() and provides a fix, which improves robustness. The refactoring into a helper method also enhances code readability and maintainability.

Medium
  • Update

@calixtus calixtus requested a review from InAnYan December 27, 2025 20:18
@jetbrains-junie
Copy link
Contributor

⚠️ It looks like a Junie's workflow file is missing. I've added it in the new PR #14740. Please review it and merge and then retry your query.

Copy link
Member

@InAnYan InAnYan left a comment

Choose a reason for hiding this comment

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

Have not run the code yet, but code-wise there is a small change.

Thanks for taking the issue!

@github-actions github-actions bot added the status: changes-required Pull requests that are not yet complete label Dec 28, 2025
@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Dec 28, 2025
@pulkitbajaj
Copy link
Contributor Author

I have updated the PR to address the architectural feedback:

  • Logic Separation: Created ChatMessageUtils in the logic module to handle content extraction using Optional<String>, as requested.
  • Type-Safety: Refactored ChatMessageComponent to use an explicit selectedText string property instead of the context menu's metadata map. This makes the selection more intuitive and type-safe.
  • Robustness: The selection logic extracts the substring during the MOUSE_PRESSED phase to ensure the state is captured before focus shifts to the context menu.

Ready for another review!

@koppor koppor added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Dec 28, 2025
@pulkitbajaj
Copy link
Contributor Author

I've applied the final refactors:

  • Replaced generic Exception with IndexOutOfBoundsException and added debug logging.
  • Switched to JabRef's ClipBoardManager using the Injector for StateManager.
  • Initialized selectedText to prevent potential NPEs.

Ready for final review!

Copy link
Member

@InAnYan InAnYan left a comment

Choose a reason for hiding this comment

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

Okay, good job, small comments

@github-actions github-actions bot added status: changes-required Pull requests that are not yet complete and removed status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers labels Dec 29, 2025
@pulkitbajaj
Copy link
Contributor Author

I have finalized the refactor and verified the logic:

  • Declarative Injection: Switched to using @Inject for the ClipBoardManager as requested.
  • Logic Polish: Implemented the early return pattern in the MOUSE_PRESSED event filter to improve readability and simplified the copy action using a standard if/else block.

Ready for the review!

Copy link
Member

@InAnYan InAnYan left a comment

Choose a reason for hiding this comment

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

Okay, almost finished

@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Dec 29, 2025
@pulkitbajaj
Copy link
Contributor Author

pulkitbajaj commented Dec 29, 2025

I have applied the final requested changes:

  • Method Extraction: Extracted the text-retrieval logic into getTextToCopy() and the selection-capture logic into extractSelectedText() to keep the setup method concise and readable.
  • Style Alignment: Updated the @Inject field declaration to the requested single-line format.

@InAnYan
Copy link
Member

InAnYan commented Dec 30, 2025

@pulkitbajaj
Can you use method MarkdownTextFlow.copySelectedText? Or not?

@pulkitbajaj
Copy link
Contributor Author

I have finalized the refactor and verified the logic:

  • Selection Stability: I added a MOUSE_PRESSED event filter to consume secondary clicks when a selection is active. In JavaFX, a right-click can trigger a focus change that clears the text highlight before the context menu action executes; consuming the event 'locks' the selection for the copy action.
  • Logic Delegation: The 'Copy' action now utilizes markdownTextFlow.copySelectedText() as suggested to handle markdown reconstruction.
  • Method Extraction: Extracted the fallback copy logic into copyFullMessage() to keep setupContextMenu concise.

calixtus
calixtus previously approved these changes Dec 31, 2025
@calixtus calixtus enabled auto-merge December 31, 2025 14:50
@subhramit
Copy link
Member

subhramit commented Dec 31, 2025

I have finalized the refactor and verified the logic:

  • Selection Stability: I added a MOUSE_PRESSED event filter to consume secondary clicks when a selection is active. In JavaFX, a right-click can trigger a focus change that clears the text highlight before the context menu action executes; consuming the event 'locks' the selection for the copy action.
  • Logic Delegation: The 'Copy' action now utilizes markdownTextFlow.copySelectedText() as suggested to handle markdown reconstruction.
  • Method Extraction: Extracted the fallback copy logic into copyFullMessage() to keep setupContextMenu concise.

Hi @pulkitbajaj
Please note that we know how to use an LLM and we would not need you a middleman if we wanted to get the issue solved in that manner. Since this PR is complete, we will not be blocking it, but you will not be allowed to contribute further for a certain period of time as a result of violating our AI usage policy.

Copy link
Member

@subhramit subhramit left a comment

Choose a reason for hiding this comment

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

Please add a Changelog entry and mark the respective mandatory check in the PR description.

@github-actions github-actions bot added the status: changes-required Pull requests that are not yet complete label Dec 31, 2025
auto-merge was automatically disabled December 31, 2025 17:24

Head branch was pushed to by a user without write access

@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Dec 31, 2025
@pulkitbajaj
Copy link
Contributor Author

I've added the changelog entry and manually updated checklist in my description. I am sorry for violating the AI usage policy and assure that this would not happen in future.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static javafx.scene.input.MouseEvent.MOUSE_PRESSED;
Copy link
Member

Choose a reason for hiding this comment

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

Don't import static, import MouseEvent and use MouseEvent.MOUSE_PRESSED

subhramit
subhramit previously approved these changes Jan 1, 2026
@github-actions github-actions bot added the status: changes-required Pull requests that are not yet complete label Jan 13, 2026
@github-actions
Copy link
Contributor

Your pull request conflicts with the target branch.

Please merge with your code. For a step-by-step guide to resolve merge conflicts, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line.

@github-actions github-actions bot removed the status: changes-required Pull requests that are not yet complete label Jan 13, 2026
@koppor koppor added the status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers label Jan 15, 2026
@Siedlerchr Siedlerchr added this pull request to the merge queue Jan 16, 2026
Merged via the queue into JabRef:main with commit 4783fa5 Jan 16, 2026
62 checks passed
Siedlerchr added a commit to st-rm-ng/jabref that referenced this pull request Jan 17, 2026
* upstream/main: (64 commits)
  New Crowdin updates (JabRef#14862)
  Make JDK25 available (JabRef#14861)
  Fix empty entries array when exporting group chat to JSON (JabRef#14814)
  feat: add right-click copy context menu to AI chat messages (JabRef#14722)
  FIX : generic error dialog and icon in Source Tab parsing (JabRef#14828)
  Factor out setup-* actions (JabRef#14859)
  Link .http files.
  Update dependency org.postgresql:postgresql to v42.7.9 (JabRef#14857)
  Add more commands to JabSrv (JabRef#14855)
  Fix JabRef#14821: Hide identifier action buttons when field is empty (JabRef#14831)
  Add GH_TOKEN to closed issues/PRs processing step
  New Crowdin updates (JabRef#14854)
  New Crowdin updates (JabRef#14849)
  Chore(deps): Bump jablib/src/main/resources/csl-styles from `0201999` to `f345aa8` (JabRef#14833)
  Add support for book front covers, again (JabRef#14777)
  Readd min width to button in new enty dialog (JabRef#14791)
  Replace plugin impl from jbang plugin (JabRef#14846)
  Revise AI policy wording
  Chore(deps): Bump jablib/src/main/resources/csl-locales (JabRef#14677)
  Update dependency com.konghq:unirest-modules-gson to v4.7.1 (JabRef#14845)
  ...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Review effort 2/5 status: ready-for-review Pull Requests that are ready to be reviewed by the maintainers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot copy text in "Chat with group" window

6 participants