Skip to content

Add PR Review Comments Dashboard to RHS sidebar#971

Open
nickmisasi wants to merge 2 commits intomasterfrom
claude/pr-review-comments-dashboard-VYBxj
Open

Add PR Review Comments Dashboard to RHS sidebar#971
nickmisasi wants to merge 2 commits intomasterfrom
claude/pr-review-comments-dashboard-VYBxj

Conversation

@nickmisasi
Copy link
Contributor

@nickmisasi nickmisasi commented Feb 26, 2026

Implement a drill-down detail view in the RHS that shows a PR's review
comments grouped by file with diff snippets, enabling replying, reacting,
resolving threads, and assigning selected feedback to AI agents — all
without leaving Mattermost.

Backend:

  • Add GraphQL query/mutation structs for review threads with pagination
  • Add executeMutation method to GraphQL client
  • Add 6 new API endpoints: pr-review-threads, pr-review-comment-reply,
    pr-review-comment-reaction, pr-resolve-thread, pr-create-comment,
    and plugin-settings/ai-agents
  • Add AI Agents configuration (Name:@mention pairs) with Claude/Cursor defaults

Frontend:

  • Add Redux infrastructure: action types, reducers, selectors, API client
    methods, and action creators for review thread management
  • Add drill-down navigation from PR list items to detail view
  • Add PRReviewDetail container with file-grouped thread display
  • Add DiffHunkDisplay with syntax-highlighted diff rendering
  • Add ReviewComment with author info, timestamps, and reaction bar
  • Add ReplyBox for inline comment replies
  • Add ResolveThread toggle for thread resolution via GraphQL
  • Add AIAssignBar for selecting comments and assigning to AI agents
  • Add ReactionBar with optimistic toggle for 6 emoji reactions

Summary by CodeRabbit

  • New Features
    • Added PR review thread management—view and interact with review comments, toggle reactions, and resolve discussion threads
    • Added AI agent assignment to streamline PR reviews by assigning AI assistants to selected comments
    • Added configurable AI agents with built-in defaults for popular AI assistants

Implement a drill-down detail view in the RHS that shows a PR's review
comments grouped by file with diff snippets, enabling replying, reacting,
resolving threads, and assigning selected feedback to AI agents — all
without leaving Mattermost.

Backend:
- Add GraphQL query/mutation structs for review threads with pagination
- Add executeMutation method to GraphQL client
- Add 6 new API endpoints: pr-review-threads, pr-review-comment-reply,
  pr-review-comment-reaction, pr-resolve-thread, pr-create-comment,
  and plugin-settings/ai-agents
- Add AI Agents configuration (Name:@mention pairs) with Claude/Cursor defaults

Frontend:
- Add Redux infrastructure: action types, reducers, selectors, API client
  methods, and action creators for review thread management
- Add drill-down navigation from PR list items to detail view
- Add PRReviewDetail container with file-grouped thread display
- Add DiffHunkDisplay with syntax-highlighted diff rendering
- Add ReviewComment with author info, timestamps, and reaction bar
- Add ReplyBox for inline comment replies
- Add ResolveThread toggle for thread resolution via GraphQL
- Add AIAssignBar for selecting comments and assigning to AI agents
- Add ReactionBar with optimistic toggle for 6 emoji reactions

https://claude.ai/code/session_01DpvWCC92Q1UUuM1AA52THr
@nickmisasi nickmisasi requested a review from a team as a code owner February 26, 2026 16:15
@nickmisasi nickmisasi added the Setup Cloud Test Server Setup a test server using Mattermost Cloud label Feb 26, 2026
@mm-cloud-bot
Copy link

Creating a Plugin SpinWick test server

@dryrunsecurity
Copy link

dryrunsecurity bot commented Feb 26, 2026

DryRun Security

🟡 Please give this pull request extra attention during review.

This pull request introduces two risky client-side issues: a prototype pollution DoS in webapp/src/selectors.ts where unsanitized thread.path can be a reserved property (e.g., proto) and cause a crash when .push is called, and a Trojan Source vulnerability in webapp/src/components/.../diff_hunk_display.tsx where diff hunks with Unicode bidirectional control characters are rendered unsanitized, allowing maliciously reordered code to be displayed to reviewers. Both issues come from trusting external PR file paths and raw diff content and should be mitigated by validating/sanitizing keys and stripping or highlighting Bidi characters.

🟡 Prototype Pollution (DoS) in webapp/src/selectors.ts (e1fcfb91)
Vulnerability Prototype Pollution (DoS)
Description The getThreadsGroupedByFile selector in webapp/src/selectors.ts uses the unsanitized thread.path as an object key on a plain JavaScript object. Since thread.path is sourced from file paths in a GitHub repository, an attacker who can create files in that repository (e.g., through a PR or direct commit) can name a file with reserved property names like __proto__, constructor, or toString. When the selector processes a review thread on such a file, it will access the corresponding property on Object.prototype. The subsequent call to .push(thread) on this prototype property will throw a TypeError because these properties are not arrays and do not have a push method, resulting in a client-side Denial of Service (DoS) and crashing the PR review sidebar.

if (!grouped[thread.path]) {
grouped[thread.path] = [];
}
grouped[thread.path].push(thread);

🟡 Deceptive Code Display (Trojan Source) in webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx (1e03608f)
Vulnerability Deceptive Code Display (Trojan Source)
Description The DiffHunkDisplay component in the Mattermost GitHub plugin renders code diffs directly from the GitHub API without sanitizing or highlighting Unicode Bidirectional (Bidi) control characters (e.g., U+202E, U+2067). This lack of sanitization allows for 'Trojan Source' attacks, where an attacker can craft a pull request containing malicious code that appears benign in the review UI by using Bidi characters to reorder the displayed text. Because the backend Go code also passes the raw diffHunk data to the frontend without any filtering, a reviewer might approve a malicious change that looks safe in the Mattermost plugin sidebar.


All finding details can be found in the DryRun Security Dashboard.

@mm-cloud-bot
Copy link

Plugin Spinwick PR #971 🎉

Test server created!

Access here: https://github-pr-971-3771b.test.mattermost.cloud

Plugin Version Artifact
github 8eee32f Download

⚠️ Plugin Installation Issue

The test server was created successfully, but there was an issue installing or enabling the plugin automatically:

  • Install Error: failed to wait for S3 artifact: timed out waiting for S3 artifact

You can manually install the plugin:

  1. Download the plugin artifact from the link above
  2. Upload it to your test server at 1wgscd6ug7fk3m89y316tnwjpc
  3. Enable it in System Console > Plugins

Future commits will still attempt to automatically update the plugin.

Installation ID: 1wgscd6ug7fk3m89y316tnwjpc
Logs: Click here

Credentials: Posted securely in this Mattermost channel - Look for PR #971

- Fix gofmt struct alignment in ReviewCommentResponse
- Fix G115 integer overflow: add bounds check before int->int32 conversion
- Fix modernize: use WaitGroup.Go instead of Add+go func+Done pattern
- Fix modernize: use strings.SplitSeq instead of strings.Split for ranging
- Fix S1016: use type conversion AIAgentResponse(a) instead of struct literal

https://claude.ai/code/session_01DpvWCC92Q1UUuM1AA52THr
@mm-cloud-bot
Copy link

New commit detected. SpinWick will update the plugin if a new artifact is available.

@mm-cloud-bot
Copy link

Plugin test server update attempted, but encountered an issue:

⚠️ Install Error: failed to wait for S3 artifact: timed out waiting for S3 artifact

The test server is still available. You can manually download and install the updated plugin using the artifact link below.

Updated with git commit d5f65194f959373c4fbd12f9e9abace953253439

Plugin Version Artifact
github d5f6519 Download

@nickmisasi nickmisasi added the Do Not Merge Should not be merged until this label is removed label Feb 26, 2026
@nickmisasi
Copy link
Contributor Author

@coderabbitai review

@nickmisasi nickmisasi requested a review from Copilot February 26, 2026 22:16
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

This PR introduces GitHub PR review thread management with AI agent assignment capabilities. It adds backend API endpoints to fetch and manipulate review threads, GraphQL integration for thread data retrieval, Redux state management for PR context and thread data, and a detailed review sidebar component with thread visualization, comment interactions, and AI agent selection.

Changes

Cohort / File(s) Summary
Plugin Configuration
plugin.json, server/plugin/configuration.go
Added AIAgents configuration setting with Name:@mention format parsing; introduced AIAgent struct and ParseAIAgents() method with defaults for Claude and Cursor.
Backend API Handlers
server/plugin/api.go
Implemented six new HTTP endpoints for PR review operations: getPRReviewThreads, replyToReviewComment, toggleReviewCommentReaction, resolveReviewThread, createPRComment, and getAIAgents with request/response handling.
GraphQL Client
server/plugin/graphql/client.go, server/plugin/graphql/review_threads.go, server/plugin/graphql/review_threads_query.go
Added executeMutation method to GraphQL client; implemented GetReviewThreads and ResolveReviewThread methods with paginated data retrieval; defined GraphQL type structures for threads, comments, reactions, and review summaries.
Frontend Action Types & Actions
webapp/src/action_types/index.ts, webapp/src/actions/index.ts
Added six action types for PR review state management; created async action creators (getPRReviewThreads, replyToReviewComment, toggleReaction, resolveThread, postAIAssignment, getAIAgents) with loading and error handling.
Frontend State & Selectors
webapp/src/reducers/index.ts, webapp/src/selectors.ts
Introduced reducers for selectedPR, prReviewThreads, prReviewThreadsLoading, and aiAgents; added selectors for accessing review data and a getThreadsGroupedByFile selector for component rendering.
Frontend Type Definitions & Client
webapp/src/types/github_types.ts, webapp/src/client/client.ts, webapp/src/constants/index.js
Extended types with SelectedPRData, ReviewThreadData, PRReviewThreadsData, AIAgent structures; implemented Client methods for API calls; added REACTIONS constant.
Sidebar Integration
webapp/src/components/sidebar_right/sidebar_right.jsx, webapp/src/components/sidebar_right/github_items.tsx, webapp/src/components/sidebar_right/index.jsx
Wired PR selection to SidebarRight; added PR click handler in GithubItems to detect and select pull requests; connected new Redux actions and state.
PR Review Detail Components
webapp/src/components/sidebar_right/pr_review_detail/index.tsx, webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx, webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail_header.tsx, webapp/src/components/sidebar_right/pr_review_detail/file_group.tsx, webapp/src/components/sidebar_right/pr_review_detail/review_thread.tsx, webapp/src/components/sidebar_right/pr_review_detail/review_comment.tsx
Implemented container and main detail component for PR review display; created header with summary metrics; added file grouping and thread rendering with selection state management.
Review Interaction Components
webapp/src/components/sidebar_right/pr_review_detail/reply_box.tsx, webapp/src/components/sidebar_right/pr_review_detail/reaction_bar.tsx, webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx, webapp/src/components/sidebar_right/pr_review_detail/ai_assign_bar.tsx
Added reply composition component with expandable textarea; implemented emoji reaction toggling; created diff hunk viewer with line-by-line syntax highlighting; added AI agent selector bar for batch assignment.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant React as React Components
    participant Redux as Redux Store
    participant Client as API Client
    participant Backend as Backend API
    participant GraphQL as GraphQL API

    User->>React: Click PR in GithubItems
    React->>Redux: dispatch selectPR(prData)
    Redux->>Redux: Store selectedPR
    React->>React: Render PRReviewDetail
    
    React->>Redux: dispatch getPRReviewThreads()
    Redux->>Client: client.getPRReviewThreads()
    Client->>Backend: GET /pr-review-threads
    Backend->>GraphQL: Execute GetReviewThreads query
    GraphQL-->>Backend: Threads, comments, reactions
    Backend-->>Client: PRReviewThreadsData
    Client-->>Redux: Response
    Redux->>Redux: Store threads & loading state
    Redux-->>React: Update selectedPR, threads, aiAgents

    React->>React: Render FileGroup & ReviewThread list

    User->>React: Click reaction on comment
    React->>Redux: dispatch toggleReaction()
    Redux->>Client: client.toggleReaction()
    Client->>Backend: POST /pr-review-comment-reaction
    Backend->>GraphQL: GitHub API mutation
    GraphQL-->>Backend: Updated reaction state
    Backend-->>React: ToggleReactionResponse
    React->>React: Update reaction display

    User->>React: Select comments & click Assign
    React->>Redux: dispatch postAIAssignment()
    Redux->>Client: client.createPRComment()
    Client->>Backend: POST /pr-create-comment
    Backend->>GraphQL: GitHub API create comment
    GraphQL-->>Backend: Created comment
    Backend-->>React: Success response
    React->>React: Clear selections, refresh threads
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main feature being added: a PR Review Comments Dashboard displayed in the right-hand sidebar (RHS).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/pr-review-comments-dashboard-VYBxj

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.5.0)

Error: unknown linters: 'modernize', run 'golangci-lint help linters' to see the list of supported linters
The command is terminated due to an error: unknown linters: 'modernize', run 'golangci-lint help linters' to see the list of supported linters


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

Copy link

@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: 9

🧹 Nitpick comments (6)
webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx (1)

25-32: Prefer theme-aware colours instead of fixed RGBA/hex values.

Hardcoded diff colours and text colours can be hard to read in some themes. Consider deriving these from theme tokens to keep contrast consistent.

Also applies to: 68-97

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx`
around lines 25 - 32, The styling currently returns hardcoded color objects for
diff lines (the branches checking line.startsWith('+'), line.startsWith('-') and
the default) — update these to use theme tokens from your styling system (e.g.,
Material-UI/theme, styled-components theme, or CSS variables) instead of literal
'rgba(...)' or '#555'; locate the function in diff_hunk_display.tsx that
produces the inline style for each line (the returns shown) and replace them
with values derived from the active theme (e.g., theme.palette.success.light,
theme.palette.error.light, theme.palette.action.hover or a dedicated token for
diff additions/deletions and text color) so colours adapt to light/dark themes
and maintain contrast; ensure you import/use the theme context (useTheme or
ThemeProvider tokens) and fall back to accessible defaults if the theme token is
missing.
server/plugin/configuration.go (1)

311-315: De-duplicate configured agents by mention before appending.

Right now duplicates are allowed, which can produce repeated agent entries and duplicate React keys in the agent selector.

♻️ Proposed refinement
 func (c *Configuration) ParseAIAgents() []AIAgent {
 	defaults := []AIAgent{
 		{Name: "Claude", Mention: "@claude", IsDefault: true},
 		{Name: "Cursor", Mention: "@cursor", IsDefault: true},
 	}
@@
-	agents := make([]AIAgent, 0, len(defaults))
-	agents = append(agents, defaults...)
+	agents := make([]AIAgent, 0, len(defaults))
+	agents = append(agents, defaults...)
+	seen := map[string]struct{}{}
+	for _, d := range defaults {
+		seen[d.Mention] = struct{}{}
+	}
@@
-		agents = append(agents, AIAgent{
+		if _, exists := seen[mention]; exists {
+			continue
+		}
+		seen[mention] = struct{}{}
+		agents = append(agents, AIAgent{
 			Name:      name,
 			Mention:   mention,
 			IsDefault: false,
 		})
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/plugin/configuration.go` around lines 311 - 315, The agents slice is
being appended with AIAgent entries without deduplicating by the Mention field,
causing duplicate agents and React key collisions; update the code that builds
agents (where agents = append(agents, AIAgent{ Name: name, Mention: mention,
IsDefault: false })) to skip appending if that mention already exists — e.g.,
maintain a local seenMentions map[string]bool, check seenMentions[mention]
before append, and mark seenMentions[mention]=true when you append so each
AIAgent (by Mention) is only added once.
webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail_header.tsx (1)

29-53: Localize header copy instead of hardcoding English strings.

The visible labels are currently embedded literals; moving them to the i18n path will keep this view consistent in non-English workspaces.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail_header.tsx`
around lines 29 - 53, Replace hardcoded English UI strings in
pr_review_detail_header.tsx with i18n keys and calls so the header is
localizable: import and use the project's translation helper (e.g., t from
react-i18next or your i18n.translate) and change literals like '\u2190 Back',
the label concatenation "{summary.approved + ' approved'}",
"{summary.changes_requested + ' changes requested'}", and
"{summary.unresolved_threads + ' unresolved threads'}" to
t('prReview.header.back'), t('prReview.header.approved', {count:
summary.approved}), t('prReview.header.changesRequested', {count:
summary.changes_requested}), and t('prReview.header.unresolvedThreads', {count:
summary.unresolved_threads}) (or your app's interpolation syntax); then add
those keys to the i18n resource files under the appropriate namespace so
translations are available.
webapp/src/components/sidebar_right/pr_review_detail/reply_box.tsx (1)

29-43: Missing error handling for failed submissions.

If onSubmit throws or rejects, the input text is cleared and the textarea collapses (lines 38-39), leaving the user with no feedback about the failure and losing their typed reply. Consider adding a catch block to preserve the text and display an error state.

♻️ Proposed fix to handle submission errors
     const handleSubmit = useCallback(async () => {
         const body = text.trim();
         if (!body || submitting) {
             return;
         }

         setSubmitting(true);
         try {
             await onSubmit(body);
             setText('');
             setExpanded(false);
+        } catch (error) {
+            // Keep text so user can retry; optionally show error feedback
+            console.error('Failed to submit reply:', error);
         } finally {
             setSubmitting(false);
         }
     }, [text, submitting, onSubmit]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/reply_box.tsx` around
lines 29 - 43, The submit handler clears the textarea and collapses the box even
when onSubmit fails; update handleSubmit (in reply_box.tsx) to wrap the await
onSubmit(body) in a try/catch so that on rejection you do NOT call setText('')
or setExpanded(false), instead set an error state (e.g., setSubmitError /
setError) and keep the text/expansion so the user can retry; keep the finally
block to setSubmitting(false) and ensure the new error state is included/cleared
appropriately when the user edits or retries.
server/plugin/api.go (2)

2214-2217: Direct struct conversion may be fragile if underlying type changes.

The type conversion AIAgentResponse(a) works because both types have identical fields in the same order. Consider explicit field assignment for clarity and resilience to future changes.

♻️ Suggested explicit conversion
 	agentList := make([]AIAgentResponse, 0, len(agents))
 	for _, a := range agents {
-		agentList = append(agentList, AIAgentResponse(a))
+		agentList = append(agentList, AIAgentResponse{
+			Name:      a.Name,
+			Mention:   a.Mention,
+			IsDefault: a.IsDefault,
+		})
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/plugin/api.go` around lines 2214 - 2217, The loop that builds
agentList uses a direct conversion AIAgentResponse(a), which is brittle; replace
it with explicit field-wise construction of AIAgentResponse inside the loop
(iterate over agents and append AIAgentResponse{<map each field of
AIAgentResponse to the corresponding field on a>}) so future changes to struct
ordering or types won't silently break; ensure you map every exported field from
the source agent to the corresponding AIAgentResponse field in the agentList
construction.

2032-2055: Consider pagination efficiency for high-reaction comments.

The current implementation fetches all reactions across all pages to find the current user's reaction. For comments with many reactions, this could result in multiple API calls. Consider breaking out of the pagination loop early once the user's reaction is found.

♻️ Suggested optimization
 	// List existing reactions to check if current user already reacted
 	var allReactions []*github.Reaction
+	var existingReactionID int64
 	opt := &github.ListOptions{PerPage: 100}
+	found := false
 	for {
 		var reactions []*github.Reaction
 		var resp *github.Response
 		var err error
 		if cErr := p.useGitHubClient(c.GHInfo, func(info *GitHubUserInfo, token *oauth2.Token) error {
 			reactions, resp, err = githubClient.Reactions.ListPullRequestCommentReactions(c.Ctx, req.Owner, req.Repo, req.CommentID, opt)
 			if err != nil {
 				return err
 			}
 			return nil
 		}); cErr != nil {
 			c.Log.WithError(cErr).Errorf("Failed to list reactions")
 			p.writeAPIError(w, &APIErrorResponse{Message: "failed to list reactions", StatusCode: http.StatusInternalServerError})
 			return
 		}
-		allReactions = append(allReactions, reactions...)
+		for _, reaction := range reactions {
+			if reaction.GetUser().GetLogin() == currentUserLogin && reaction.GetContent() == req.Reaction {
+				existingReactionID = reaction.GetID()
+				found = true
+				break
+			}
+		}
+		if found {
+			break
+		}
 		if resp.NextPage == 0 {
 			break
 		}
 		opt.Page = resp.NextPage
 	}
-
-	// Check if current user already reacted with this content
-	var existingReactionID int64
-	for _, reaction := range allReactions {
-		if reaction.GetUser().GetLogin() == currentUserLogin && reaction.GetContent() == req.Reaction {
-			existingReactionID = reaction.GetID()
-			break
-		}
-	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/plugin/api.go` around lines 2032 - 2055, The pagination loop collects
allReactions by calling githubClient.Reactions.ListPullRequestCommentReactions
repeatedly; update this loop to stop paging early when the current user's
reaction is found: after each successful ListPullRequestCommentReactions call
(inside the loop after reactions are returned), scan the reactions slice for a
reaction whose User.Login or User.ID matches the current user (use
c.GHInfo.Login or ID), append the reactions to allReactions as before, and if a
match is found set a flag and break out of the pagination loop (avoid further
pages and API calls). Keep existing error handling around p.useGitHubClient and
github call intact, but add the user-match check and early break to avoid
unnecessary requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/plugin/graphql/review_threads_query.go`:
- Around line 52-85: The package-level mutable GraphQL payload structs
reviewThreadsQuery, resolveThreadMutation, and unresolveThreadMutation are
shared across requests and cause data races when passed to
executeQuery/executeMutation; fix by making those structs local per-request:
remove or stop using the package-level vars and instead allocate new instances
of the same struct types inside the functions that call
executeQuery/executeMutation (or deep-copy before/after use), updating callers
to pass the local instance and read results from it so concurrent handlers no
longer share mutable state.

In `@webapp/src/components/sidebar_right/github_items.tsx`:
- Around line 285-287: The PR card container currently intercepts clicks from
nested anchor tags and lacks keyboard accessibility; update the container (where
style.container, isPR, props.onSelectPR and handlePRClick are used) to: 1) add
keyboard semantics (role="button" and tabIndex={0}) and an onKeyDown handler
that triggers the same action as handlePRClick for Enter and Space; and 2)
prevent hijacking nested anchor links by guarding the click/keyboard handler to
return early when the event originated from or is inside an anchor (check
event.target or walk up via event.target.closest('a') or respect
event.defaultPrevented) so nested <a> elements (e.g., userName and title/number
links) keep their default navigation. Ensure props.onSelectPR is still called
only when appropriate.

In `@webapp/src/components/sidebar_right/pr_review_detail/ai_assign_bar.tsx`:
- Line 4: The selectedAgent state initialized with useState(defaultMention) can
stay empty if agents load after initial render; update selectedAgent whenever
agents prop changes by adding a useEffect that computes the appropriate default
(same logic as defaultMention) and calls setSelectedAgent, and ensure any
handlers like onAssign or assign callback use the latest selectedAgent (refer to
selectedAgent, setSelectedAgent, defaultMention, and agents in
ai_assign_bar.tsx) so assignments no longer silently do nothing when agents
arrive late.

In `@webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx`:
- Around line 49-63: The two JSX <button> elements that toggle expansion (the
ones with onClick={() => setExpanded(true)} and onClick={() =>
setExpanded(false)}) must have an explicit type='button' to avoid submitting a
surrounding form; update both buttons in diff_hunk_display.tsx to include
type='button' while keeping their existing onClick and styles
(styles.expandButton) intact.
- Around line 38-47: The current JSX inside the <pre> in diff_hunk_display.tsx
uses <div> elements for each line (visibleLines.map), which is invalid as <pre>
must contain phrasing content; replace those inner <div> tags with <span>
elements and preserve block layout by merging display:'block' into the existing
style (e.g., style={{...styles.line, ...getLineStyle(line), display: 'block'}})
while keeping the key, visibleLines iteration, getLineStyle, and styles.* usages
unchanged.

In `@webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx`:
- Around line 66-77: The handleAIAssign callback clears the selection
immediately because postAIAssignment is not awaited; wrap the call to
actions.postAIAssignment in an async function (or make handleAIAssign async),
await it inside a try/catch, and only call setSelectedCommentIds(new Set())
after a successful await; in the catch block surface the error to the user
(e.g., via an existing actions.notifyError or setError state) so failures do not
silently drop the selection. Use the function names handleAIAssign,
postAIAssignment, setSelectedCommentIds, and actions to locate and update the
code.
- Around line 157-163: The loadingSpinner style uses animation 'spin' but the
`@keyframes` spin is missing; add the keyframes animation named "spin" (to rotate
360deg) and ensure it's loaded globally so the component's animation runs.
Define the `@keyframes` spin in a CSS/SCSS file or in your global
stylesheet/plugin initialization and import it into the app, or inject it into
the global styles used by the PRReviewDetail component so the loadingSpinner
animation is recognized at runtime.

In `@webapp/src/components/sidebar_right/pr_review_detail/reaction_bar.tsx`:
- Around line 52-72: The handler handleToggle currently reads
localReactions[content] from a stale closure; change it to perform the
optimistic update using the functional updater of setLocalReactions (compute
current = prev[content] inside the updater) and remove direct reads of
localReactions from the closure and from the dependency array; to support proper
revert on error, capture the previous snapshot into a ref (e.g.,
prevReactionsRef) immediately before the optimistic update and, on catch,
restore that snapshot via setLocalReactions(prev => prevReactionsRef.current) —
update references to handleToggle, setLocalReactions, localReactions, and
onToggleReaction accordingly.

In `@webapp/src/reducers/index.ts`:
- Around line 264-270: prReviewThreadsLoading currently only responds to
ActionTypes.PR_REVIEW_THREADS_LOADING and can stay true after the selected PR is
cleared; update the reducer function prReviewThreadsLoading to also handle the
action type that clears the selected PR (e.g., ActionTypes.CLEAR_SELECTED_PR)
and return false in that case so the loading flag resets when the PR is cleared.

---

Nitpick comments:
In `@server/plugin/api.go`:
- Around line 2214-2217: The loop that builds agentList uses a direct conversion
AIAgentResponse(a), which is brittle; replace it with explicit field-wise
construction of AIAgentResponse inside the loop (iterate over agents and append
AIAgentResponse{<map each field of AIAgentResponse to the corresponding field on
a>}) so future changes to struct ordering or types won't silently break; ensure
you map every exported field from the source agent to the corresponding
AIAgentResponse field in the agentList construction.
- Around line 2032-2055: The pagination loop collects allReactions by calling
githubClient.Reactions.ListPullRequestCommentReactions repeatedly; update this
loop to stop paging early when the current user's reaction is found: after each
successful ListPullRequestCommentReactions call (inside the loop after reactions
are returned), scan the reactions slice for a reaction whose User.Login or
User.ID matches the current user (use c.GHInfo.Login or ID), append the
reactions to allReactions as before, and if a match is found set a flag and
break out of the pagination loop (avoid further pages and API calls). Keep
existing error handling around p.useGitHubClient and github call intact, but add
the user-match check and early break to avoid unnecessary requests.

In `@server/plugin/configuration.go`:
- Around line 311-315: The agents slice is being appended with AIAgent entries
without deduplicating by the Mention field, causing duplicate agents and React
key collisions; update the code that builds agents (where agents =
append(agents, AIAgent{ Name: name, Mention: mention, IsDefault: false })) to
skip appending if that mention already exists — e.g., maintain a local
seenMentions map[string]bool, check seenMentions[mention] before append, and
mark seenMentions[mention]=true when you append so each AIAgent (by Mention) is
only added once.

In `@webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx`:
- Around line 25-32: The styling currently returns hardcoded color objects for
diff lines (the branches checking line.startsWith('+'), line.startsWith('-') and
the default) — update these to use theme tokens from your styling system (e.g.,
Material-UI/theme, styled-components theme, or CSS variables) instead of literal
'rgba(...)' or '#555'; locate the function in diff_hunk_display.tsx that
produces the inline style for each line (the returns shown) and replace them
with values derived from the active theme (e.g., theme.palette.success.light,
theme.palette.error.light, theme.palette.action.hover or a dedicated token for
diff additions/deletions and text color) so colours adapt to light/dark themes
and maintain contrast; ensure you import/use the theme context (useTheme or
ThemeProvider tokens) and fall back to accessible defaults if the theme token is
missing.

In
`@webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail_header.tsx`:
- Around line 29-53: Replace hardcoded English UI strings in
pr_review_detail_header.tsx with i18n keys and calls so the header is
localizable: import and use the project's translation helper (e.g., t from
react-i18next or your i18n.translate) and change literals like '\u2190 Back',
the label concatenation "{summary.approved + ' approved'}",
"{summary.changes_requested + ' changes requested'}", and
"{summary.unresolved_threads + ' unresolved threads'}" to
t('prReview.header.back'), t('prReview.header.approved', {count:
summary.approved}), t('prReview.header.changesRequested', {count:
summary.changes_requested}), and t('prReview.header.unresolvedThreads', {count:
summary.unresolved_threads}) (or your app's interpolation syntax); then add
those keys to the i18n resource files under the appropriate namespace so
translations are available.

In `@webapp/src/components/sidebar_right/pr_review_detail/reply_box.tsx`:
- Around line 29-43: The submit handler clears the textarea and collapses the
box even when onSubmit fails; update handleSubmit (in reply_box.tsx) to wrap the
await onSubmit(body) in a try/catch so that on rejection you do NOT call
setText('') or setExpanded(false), instead set an error state (e.g.,
setSubmitError / setError) and keep the text/expansion so the user can retry;
keep the finally block to setSubmitting(false) and ensure the new error state is
included/cleared appropriately when the user edits or retries.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 65c6511 and d5f6519.

📒 Files selected for processing (26)
  • plugin.json
  • server/plugin/api.go
  • server/plugin/configuration.go
  • server/plugin/graphql/client.go
  • server/plugin/graphql/review_threads.go
  • server/plugin/graphql/review_threads_query.go
  • webapp/src/action_types/index.ts
  • webapp/src/actions/index.ts
  • webapp/src/client/client.ts
  • webapp/src/components/sidebar_right/github_items.tsx
  • webapp/src/components/sidebar_right/index.jsx
  • webapp/src/components/sidebar_right/pr_review_detail/ai_assign_bar.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/file_group.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/index.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail_header.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/reaction_bar.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/reply_box.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/review_comment.tsx
  • webapp/src/components/sidebar_right/pr_review_detail/review_thread.tsx
  • webapp/src/components/sidebar_right/sidebar_right.jsx
  • webapp/src/constants/index.js
  • webapp/src/reducers/index.ts
  • webapp/src/selectors.ts
  • webapp/src/types/github_types.ts

Comment on lines +52 to +85
var reviewThreadsQuery struct {
Repository struct {
PullRequest struct {
Reviews struct {
Nodes []reviewSummaryNode
} `graphql:"reviews(first:100)"`
ReviewThreads struct {
Nodes []reviewThreadNode
PageInfo struct {
EndCursor githubv4.String
HasNextPage bool
}
} `graphql:"reviewThreads(first:50, after:$threadsCursor)"`
} `graphql:"pullRequest(number:$prNumber)"`
} `graphql:"repository(owner:$owner, name:$name)"`
}

var resolveThreadMutation struct {
ResolveReviewThread struct {
Thread struct {
ID githubv4.String
IsResolved githubv4.Boolean
}
} `graphql:"resolveReviewThread(input:$input)"`
}

var unresolveThreadMutation struct {
UnresolveReviewThread struct {
Thread struct {
ID githubv4.String
IsResolved githubv4.Boolean
}
} `graphql:"unresolveReviewThread(input:$input)"`
}
Copy link

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:

#!/bin/bash
# Find how the global query/mutation structs are used
rg -n -C3 '\b(reviewThreadsQuery|resolveThreadMutation|unresolveThreadMutation)\b' server/plugin/graphql/

Repository: mattermost/mattermost-plugin-github

Length of output: 3891


🏁 Script executed:

#!/bin/bash
# Check for address-of operations on these variables
rg -n -C3 '&reviewThreadsQuery|&resolveThreadMutation|&unresolveThreadMutation' server/plugin/graphql/

Repository: mattermost/mattermost-plugin-github

Length of output: 1587


🏁 Script executed:

#!/bin/bash
# Look for Query/Mutate execution patterns
rg -n -C5 'client\.Query|client\.Mutate|\bQuery\s*\(|\bMutate\s*\(' server/plugin/graphql/review_threads_query.go

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Check if this file is called in concurrent contexts
rg -n -C5 'goroutine|go ' server/plugin/graphql/review_threads_query.go

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

#!/bin/bash
# Find the executeQuery and executeMutation implementations
rg -n -B2 -A10 'func.*executeQuery|func.*executeMutation' server/plugin/graphql/

Repository: mattermost/mattermost-plugin-github

Length of output: 1374


🏁 Script executed:

#!/bin/bash
# Check the full review_threads.go to understand the calling context
head -100 server/plugin/graphql/review_threads.go

Repository: mattermost/mattermost-plugin-github

Length of output: 3503


🏁 Script executed:

#!/bin/bash
# Look for how the review threads functions are called (potential concurrent callers)
rg -n 'FetchReviewThreads|ResolveReviewThread|UnresolveReviewThread' server/plugin/ --type go

Repository: mattermost/mattermost-plugin-github

Length of output: 1075


🏁 Script executed:

#!/bin/bash
# Verify these are public methods that could be called from concurrent HTTP handlers
grep -n "^func.*GetReviewThreads\|^func.*ResolveReviewThread" server/plugin/graphql/review_threads.go

Repository: mattermost/mattermost-plugin-github

Length of output: 311


🏁 Script executed:

#!/bin/bash
# Check the api.go call site context to confirm HTTP handler concurrency
sed -n '2135,2150p' server/plugin/api.go

Repository: mattermost/mattermost-plugin-github

Length of output: 780


Avoid shared mutable GraphQL payload structs across concurrent requests.

Lines 52, 69, and 78 introduce package-level mutable containers (reviewThreadsQuery, resolveThreadMutation, unresolveThreadMutation). These are passed by reference to executeQuery and executeMutation, which populate them via the underlying GraphQL client. When these public methods are called concurrently from HTTP handlers, the shared globals can race—requests overwrite each other's data in the same struct fields. Create per-request local instances instead, or deep-copy the results before reuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/plugin/graphql/review_threads_query.go` around lines 52 - 85, The
package-level mutable GraphQL payload structs reviewThreadsQuery,
resolveThreadMutation, and unresolveThreadMutation are shared across requests
and cause data races when passed to executeQuery/executeMutation; fix by making
those structs local per-request: remove or stop using the package-level vars and
instead allocate new instances of the same struct types inside the functions
that call executeQuery/executeMutation (or deep-copy before/after use), updating
callers to pass the local instance and read results from it so concurrent
handlers no longer share mutable state.

Comment on lines +285 to 287
style={{...style.container, ...(isPR && props.onSelectPR ? {cursor: 'pointer'} : {})}}
{...(isPR && props.onSelectPR ? {onClick: handlePRClick} : {})}
>
Copy link

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:

#!/bin/bash
# First, locate and examine the file
find . -type f -name "github_items.tsx" | head -20

Repository: mattermost/mattermost-plugin-github

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Read the file to understand the structure around lines 285-287
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '270,310p'

Repository: mattermost/mattermost-plugin-github

Length of output: 1845


🏁 Script executed:

#!/bin/bash
# Get the full handlePRClick function implementation
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '230,280p'

Repository: mattermost/mattermost-plugin-github

Length of output: 2235


🏁 Script executed:

#!/bin/bash
# Check for nested elements within the div (look for anchor/button children)
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '285,350p'

Repository: mattermost/mattermost-plugin-github

Length of output: 3000


🏁 Script executed:

#!/bin/bash
# Search for handlePRClick function definition
rg "const handlePRClick|function handlePRClick" -A 10 webapp/src/components/sidebar_right/github_items.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 595


🏁 Script executed:

#!/bin/bash
# Check the entire component structure to see dependencies and props
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '1,50p'

Repository: mattermost/mattermost-plugin-github

Length of output: 2646


🏁 Script executed:

#!/bin/bash
# Look for all interactive elements in the rendered content
rg "<a|<button|<input" webapp/src/components/sidebar_right/github_items.tsx -n

Repository: mattermost/mattermost-plugin-github

Length of output: 167


🏁 Script executed:

#!/bin/bash
# Get a broader view of the component structure to see all nested content
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '200,330p'

Repository: mattermost/mattermost-plugin-github

Length of output: 5953


🏁 Script executed:

#!/bin/bash
# Check the milestone variable and reviews variable definitions
cat -n webapp/src/components/sidebar_right/github_items.tsx | sed -n '50,200p'

Repository: mattermost/mattermost-plugin-github

Length of output: 6323


🏁 Script executed:

#!/bin/bash
# Check if there are any other event handlers that might interfere
rg "onClick|onKeyDown" webapp/src/components/sidebar_right/github_items.tsx -n

Repository: mattermost/mattermost-plugin-github

Length of output: 162


🏁 Script executed:

#!/bin/bash
# Verify the component doesn't have any stopPropagation calls on nested elements
rg "stopPropagation|preventDefault" webapp/src/components/sidebar_right/github_items.tsx -n

Repository: mattermost/mattermost-plugin-github

Length of output: 61


Add keyboard support and prevent nested link click hijacking on PR cards.

The container <div> at line 283 intercepts all clicks from nested anchors (userName link at line 309, and title/number links at lines 113 and 125), preventing their default navigation. Additionally, the div is mouse-only and lacks keyboard semantics, making it inaccessible to keyboard-only users.

Proposed fix
-        const handlePRClick = () => {
+        const handlePRClick = (e?: React.MouseEvent<HTMLElement> | React.KeyboardEvent<HTMLElement>) => {
+            const target = e?.target as HTMLElement | null;
+            if (target?.closest('a, button')) {
+                return;
+            }
             if (isPR && props.onSelectPR && repoName) {
                 const parts = repoName.split('/');
                 if (parts.length === 2) {
@@
             <div
                 key={item.id}
                 style={{...style.container, ...(isPR && props.onSelectPR ? {cursor: 'pointer'} : {})}}
-                {...(isPR && props.onSelectPR ? {onClick: handlePRClick} : {})}
+                {...(isPR && props.onSelectPR ? {
+                    role: 'button',
+                    tabIndex: 0,
+                    onClick: handlePRClick,
+                    onKeyDown: (e: React.KeyboardEvent<HTMLElement>) => {
+                        if (e.key === 'Enter' || e.key === ' ') {
+                            e.preventDefault();
+                            handlePRClick(e);
+                        }
+                    },
+                } : {})}
             >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/github_items.tsx` around lines 285 - 287,
The PR card container currently intercepts clicks from nested anchor tags and
lacks keyboard accessibility; update the container (where style.container, isPR,
props.onSelectPR and handlePRClick are used) to: 1) add keyboard semantics
(role="button" and tabIndex={0}) and an onKeyDown handler that triggers the same
action as handlePRClick for Enter and Space; and 2) prevent hijacking nested
anchor links by guarding the click/keyboard handler to return early when the
event originated from or is inside an anchor (check event.target or walk up via
event.target.closest('a') or respect event.defaultPrevented) so nested <a>
elements (e.g., userName and title/number links) keep their default navigation.
Ensure props.onSelectPR is still called only when appropriate.

// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

import React, {useState, useCallback} from 'react';
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Sync selectedAgent when agents updates after initial render.

useState(defaultMention) only runs once. If agents load later, the selected value can remain empty and assignment silently does nothing.

🐛 Proposed fix
-import React, {useState, useCallback} from 'react';
+import React, {useState, useCallback, useEffect} from 'react';
@@
-    const defaultMention = agents.length > 0 ? (agents.find((a) => a.is_default)?.mention || agents[0].mention) : '';
-    const [selectedAgent, setSelectedAgent] = useState(defaultMention);
+    const [selectedAgent, setSelectedAgent] = useState('');
+
+    useEffect(() => {
+        if (agents.length === 0) {
+            setSelectedAgent('');
+            return;
+        }
+
+        const nextDefault = agents.find((a) => a.is_default)?.mention || agents[0].mention;
+        if (!selectedAgent || !agents.some((a) => a.mention === selectedAgent)) {
+            setSelectedAgent(nextDefault);
+        }
+    }, [agents, selectedAgent]);

Also applies to: 19-21

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/ai_assign_bar.tsx` at
line 4, The selectedAgent state initialized with useState(defaultMention) can
stay empty if agents load after initial render; update selectedAgent whenever
agents prop changes by adding a useEffect that computes the appropriate default
(same logic as defaultMention) and calls setSelectedAgent, and ensure any
handlers like onAssign or assign callback use the latest selectedAgent (refer to
selectedAgent, setSelectedAgent, defaultMention, and agents in
ai_assign_bar.tsx) so assignments no longer silently do nothing when agents
arrive late.

Comment on lines +38 to +47
<pre style={styles.pre}>
{visibleLines.map((line, idx) => (
<div
key={idx}
style={{...styles.line, ...getLineStyle(line)}}
>
{line}
</div>
))}
</pre>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd "diff_hunk_display.tsx" -t f

Repository: mattermost/mattermost-plugin-github

Length of output: 153


🏁 Script executed:

cat -n webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 3584


🌐 Web query:

HTML5 specification div inside pre tag validity

💡 Result:

In the HTML5/WHATWG spec, a <pre> element’s content model is “phrasing content”. (html.spec.whatwg.org)
A <div> is flow content (not phrasing content), so a <div> is not a valid child of <pre> (the markup is non‑conforming). (developer.mozilla.org)

If you need indentation/grouping inside a <pre>, use phrasing elements (e.g., <span>, <code>) and CSS, or apply white-space: pre; to a container instead of using <pre>. (developer.mozilla.org)

Citations:


Replace <div> with <span> inside <pre> element.

The <pre> element's content model is restricted to phrasing content (per HTML5 spec). The <div> elements at lines 40-45 are flow content and therefore invalid children of <pre>, which can cause rendering inconsistencies. Use <span style={{display: 'block'}}> instead to maintain valid markup while preserving block-level visual presentation.

Proposed fix
-            <pre style={styles.pre}>
+            <pre style={styles.pre}>
                 {visibleLines.map((line, idx) => (
-                    <div
+                    <span
                         key={idx}
-                        style={{...styles.line, ...getLineStyle(line)}}
+                        style={{...styles.line, ...getLineStyle(line), display: 'block'}}
                     >
                         {line}
-                    </div>
+                    </span>
                 ))}
             </pre>
📝 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
<pre style={styles.pre}>
{visibleLines.map((line, idx) => (
<div
key={idx}
style={{...styles.line, ...getLineStyle(line)}}
>
{line}
</div>
))}
</pre>
<pre style={styles.pre}>
{visibleLines.map((line, idx) => (
<span
key={idx}
style={{...styles.line, ...getLineStyle(line), display: 'block'}}
>
{line}
</span>
))}
</pre>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx`
around lines 38 - 47, The current JSX inside the <pre> in diff_hunk_display.tsx
uses <div> elements for each line (visibleLines.map), which is invalid as <pre>
must contain phrasing content; replace those inner <div> tags with <span>
elements and preserve block layout by merging display:'block' into the existing
style (e.g., style={{...styles.line, ...getLineStyle(line), display: 'block'}})
while keeping the key, visibleLines iteration, getLineStyle, and styles.* usages
unchanged.

Comment on lines +49 to +63
<button
style={styles.expandButton}
onClick={() => setExpanded(true)}
>
{'Show more (' + (lines.length - MAX_VISIBLE_LINES) + ' more lines)'}
</button>
)}
{isLong && expanded && (
<button
style={styles.expandButton}
onClick={() => setExpanded(false)}
>
{'Show less'}
</button>
)}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Set button type explicitly.

Line 49 and Line 57 should set type='button' to prevent unintended submit behaviour when rendered inside forms.

Proposed fix
                 <button
+                    type='button'
                     style={styles.expandButton}
                     onClick={() => setExpanded(true)}
                 >
@@
                 <button
+                    type='button'
                     style={styles.expandButton}
                     onClick={() => setExpanded(false)}
                 >
📝 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
<button
style={styles.expandButton}
onClick={() => setExpanded(true)}
>
{'Show more (' + (lines.length - MAX_VISIBLE_LINES) + ' more lines)'}
</button>
)}
{isLong && expanded && (
<button
style={styles.expandButton}
onClick={() => setExpanded(false)}
>
{'Show less'}
</button>
)}
<button
type='button'
style={styles.expandButton}
onClick={() => setExpanded(true)}
>
{'Show more (' + (lines.length - MAX_VISIBLE_LINES) + ' more lines)'}
</button>
)}
{isLong && expanded && (
<button
type='button'
style={styles.expandButton}
onClick={() => setExpanded(false)}
>
{'Show less'}
</button>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/diff_hunk_display.tsx`
around lines 49 - 63, The two JSX <button> elements that toggle expansion (the
ones with onClick={() => setExpanded(true)} and onClick={() =>
setExpanded(false)}) must have an explicit type='button' to avoid submitting a
surrounding form; update both buttons in diff_hunk_display.tsx to include
type='button' while keeping their existing onClick and styles
(styles.expandButton) intact.

Comment on lines +66 to +77
const handleAIAssign = useCallback((agentMention: string) => {
if (selectedCommentIds.size === 0 || !selectedPR) {
return;
}

// Build a comment body mentioning the agent and referencing selected comment IDs
const commentRefs = Array.from(selectedCommentIds).join(', ');
const body = `${agentMention} Please review the following comment threads: ${commentRefs}`;

actions.postAIAssignment(selectedPR.owner, selectedPR.repo, selectedPR.number, body);
setSelectedCommentIds(new Set());
}, [selectedCommentIds, actions, selectedPR]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle errors from postAIAssignment before clearing selection.

The postAIAssignment call on line 75 is not awaited, and the selection is cleared immediately on line 76 regardless of success or failure. If the assignment fails, the user loses their selection with no feedback.

🛡️ Proposed fix to await and handle errors
     const handleAIAssign = useCallback((agentMention: string) => {
+    const handleAIAssign = useCallback(async (agentMention: string) => {
         if (selectedCommentIds.size === 0 || !selectedPR) {
             return;
         }

         // Build a comment body mentioning the agent and referencing selected comment IDs
         const commentRefs = Array.from(selectedCommentIds).join(', ');
         const body = `${agentMention} Please review the following comment threads: ${commentRefs}`;

-        actions.postAIAssignment(selectedPR.owner, selectedPR.repo, selectedPR.number, body);
-        setSelectedCommentIds(new Set());
+        try {
+            await actions.postAIAssignment(selectedPR.owner, selectedPR.repo, selectedPR.number, body);
+            setSelectedCommentIds(new Set());
+        } catch (error) {
+            console.error('Failed to post AI assignment:', error);
+            // Keep selection so user can retry
+        }
     }, [selectedCommentIds, actions, selectedPR]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx`
around lines 66 - 77, The handleAIAssign callback clears the selection
immediately because postAIAssignment is not awaited; wrap the call to
actions.postAIAssignment in an async function (or make handleAIAssign async),
await it inside a try/catch, and only call setSelectedCommentIds(new Set())
after a successful await; in the catch block surface the error to the user
(e.g., via an existing actions.notifyError or setError state) so failures do not
silently drop the selection. Use the function names handleAIAssign,
postAIAssignment, setSelectedCommentIds, and actions to locate and update the
code.

Comment on lines +157 to +163
loadingSpinner: {
width: '24px',
height: '24px',
border: '3px solid rgba(0, 0, 0, 0.1)',
borderRadius: '50%',
animation: 'spin 1s linear infinite',
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n '@keyframes\s+spin' webapp/

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Also search more broadly for spin animation
rg -n 'spin' webapp/src/components/sidebar_right/pr_review_detail/ -A 2 -B 2

Repository: mattermost/mattermost-plugin-github

Length of output: 616


🏁 Script executed:

# Check the full file to see imports and context
wc -l webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 156


🏁 Script executed:

# Check for global CSS files that might contain the animation
fd -e css -e scss -e less webapp/src/ | head -20

Repository: mattermost/mattermost-plugin-github

Length of output: 428


🏁 Script executed:

# Find CSS files in webapp
fd -e css -e scss -e less . webapp/src/

Repository: mattermost/mattermost-plugin-github

Length of output: 125


🏁 Script executed:

# Read the tsx file to see imports and check if styles are imported
head -50 webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 2042


🏁 Script executed:

# Check for any global CSS or style imports
rg -n 'import.*css|import.*style|@import' webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Search for spin animation more broadly (not just `@keyframes`)
rg -n 'spin' webapp/src/ | grep -i 'keyframe\|animation\|css' | head -20

Repository: mattermost/mattermost-plugin-github

Length of output: 202


🏁 Script executed:

# Read more of the file to see the complete styles object
sed -n '150,172p' webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 636


🏁 Script executed:

# Check for global CSS or style files at the root
find webapp/ -maxdepth 2 -name '*.css' -o -name '*.scss' -o -name 'global*' -o -name 'index.css'

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check if there's a CSS-in-JS solution or if `@keyframes` is defined dynamically
rg -n '@keyframes' webapp/ --type css --type scss

Repository: mattermost/mattermost-plugin-github

Length of output: 107


🏁 Script executed:

# Check the entire styles object to understand context
sed -n '140,172p' webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 876


🏁 Script executed:

# Check if the loadingSpinner style is actually used anywhere
rg -n 'loadingSpinner' webapp/src/components/sidebar_right/pr_review_detail/

Repository: mattermost/mattermost-plugin-github

Length of output: 354


🏁 Script executed:

# Search for any keyframes definition anywhere in the project
rg -n '@keyframes' webapp/ -g '!node_modules'

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check main app files for global styles
find webapp/src -maxdepth 1 -type f \( -name '*.css' -o -name '*.tsx' -o -name '*.ts' \)

Repository: mattermost/mattermost-plugin-github

Length of output: 130


🏁 Script executed:

# Check if there's any HTML file or global style injection
find webapp/src -maxdepth 2 -name 'index.html' -o -name 'app.tsx' -o -name 'app.ts'

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check the public folder for global styles
find webapp/public -type f -name '*.css' 2>/dev/null

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Look at the manifest or entry point files
cat webapp/src/manifest.test.ts | head -30

Repository: mattermost/mattermost-plugin-github

Length of output: 410


🏁 Script executed:

# Search for any global style definitions or CSS injection
rg -n 'global|GlobalStyle|injectGlobal' webapp/src/

Repository: mattermost/mattermost-plugin-github

Length of output: 161


🏁 Script executed:

# Check the index.js file to see if any CSS is being injected
cat webapp/src/index.js | head -150

Repository: mattermost/mattermost-plugin-github

Length of output: 5428


🏁 Script executed:

# Check for any style tag or CSS injection in the plugin class
rg -n 'style\|css\|keyframes' webapp/src/index.js -A 2 -B 2

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Look at package.json to see what CSS-in-JS or styling libraries are used
cat webapp/package.json 2>/dev/null || cat package.json 2>/dev/null | grep -A 20 '"dependencies"'

Repository: mattermost/mattermost-plugin-github

Length of output: 4341


🏁 Script executed:

# Check if there's a global CSS file being injected or referenced anywhere
rg -n 'link rel.*css|<style|importCSS|injectCSS' webapp/src/ -g '!node_modules'

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Look more carefully at how the component applies styles
sed -n '100,120p' webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 1139


🏁 Script executed:

# Check if there's any CSS file in the sidebar_right component folder
find webapp/src/components/sidebar_right -name '*.css'

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check the entire component folder structure
ls -la webapp/src/components/sidebar_right/

Repository: mattermost/mattermost-plugin-github

Length of output: 460


🏁 Script executed:

# Check if there's any global setup file that might define animations
find webapp/src -name 'setup*' -o -name 'global*' -o -name 'index.*' | grep -v node_modules

Repository: mattermost/mattermost-plugin-github

Length of output: 1077


🏁 Script executed:

# Check the LinkTooltip component CSS since we found tooltip.css
cat webapp/src/components/link_tooltip/tooltip.css

Repository: mattermost/mattermost-plugin-github

Length of output: 2806


🏁 Script executed:

# Check the entire sidebar_right folder structure
ls -la webapp/src/components/sidebar_right/pr_review_detail/

Repository: mattermost/mattermost-plugin-github

Length of output: 862


🏁 Script executed:

# Look for any emotion/CSS-in-JS global style definitions
rg -n 'Global|injectGlobal|css`|styled' webapp/src/ | head -20

Repository: mattermost/mattermost-plugin-github

Length of output: 1922


🏁 Script executed:

# Check if the Mattermost main app provides global animations
# Look at what mattermost-redux or mattermost-webapp might provide
rg -n 'animation.*spin|keyframes.*spin' webapp/node_modules/ 2>/dev/null | head -5

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check the index.tsx export to understand how the component is used
cat webapp/src/components/sidebar_right/pr_review_detail/index.tsx

Repository: mattermost/mattermost-plugin-github

Length of output: 1463


🏁 Script executed:

# Check if there's any CSS animation defined outside of webapp folder
find . -maxdepth 2 -name '*.css' -o -name '*.scss' | grep -v node_modules | xargs grep -l 'keyframes\|spin' 2>/dev/null

Repository: mattermost/mattermost-plugin-github

Length of output: 63


🏁 Script executed:

# Check if mattermost-redux or mattermost-webapp provides spin animation
find webapp/node_modules/mattermost-redux -name '*.css' 2>/dev/null | xargs grep -l 'spin' 2>/dev/null | head -5

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Check if there are any global styles being injected by Mattermost
rg -n 'spin' webapp/node_modules/@mattermost 2>/dev/null | head -10

Repository: mattermost/mattermost-plugin-github

Length of output: 61


🏁 Script executed:

# Look for any animation definitions in the entire project (not just webapp)
find . -name '*.css' -o -name '*.scss' | grep -v node_modules | xargs grep -i '@keyframes.*spin\|animation.*spin' 2>/dev/null

Repository: mattermost/mattermost-plugin-github

Length of output: 63


CSS animation spin is not defined.

The loadingSpinner style at line 162 references animation: 'spin 1s linear infinite', but no @keyframes spin definition exists anywhere in the codebase. The spinner element will render with the correct styling but won't animate.

Add the missing keyframes definition:

`@keyframes` spin {
    to {
        transform: rotate(360deg);
    }
}

Then either define it in a CSS file and import it, or inject it globally via the plugin initialization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/pr_review_detail.tsx`
around lines 157 - 163, The loadingSpinner style uses animation 'spin' but the
`@keyframes` spin is missing; add the keyframes animation named "spin" (to rotate
360deg) and ensure it's loaded globally so the component's animation runs.
Define the `@keyframes` spin in a CSS/SCSS file or in your global
stylesheet/plugin initialization and import it into the app, or inject it into
the global styles used by the PRReviewDetail component so the loadingSpinner
animation is recognized at runtime.

Comment on lines +52 to +72
const handleToggle = useCallback(async (content: string) => {
const current = localReactions[content];
const newReacted = !current.reacted;
const newCount = newReacted ? current.count + 1 : Math.max(0, current.count - 1);

// Optimistic update
setLocalReactions((prev) => ({
...prev,
[content]: {count: newCount, reacted: newReacted},
}));

try {
await onToggleReaction(content);
} catch {
// Revert on error
setLocalReactions((prev) => ({
...prev,
[content]: {count: current.count, reacted: current.reacted},
}));
}
}, [localReactions, onToggleReaction]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid stale-closure reads in optimistic reaction toggles.

Using localReactions[content] from closure can drift under quick repeated clicks. Compute from the functional updater’s prev snapshot instead.

🛠️ Suggested adjustment
-    const handleToggle = useCallback(async (content: string) => {
-        const current = localReactions[content];
-        const newReacted = !current.reacted;
-        const newCount = newReacted ? current.count + 1 : Math.max(0, current.count - 1);
+    const handleToggle = useCallback(async (content: string) => {
+        let previous = {count: 0, reacted: false};
+        let next = {count: 0, reacted: false};
 
-        // Optimistic update
         setLocalReactions((prev) => ({
             ...prev,
-            [content]: {count: newCount, reacted: newReacted},
+            [content]: (() => {
+                const current = prev[content] || {count: 0, reacted: false};
+                previous = current;
+                const reacted = !current.reacted;
+                next = {count: reacted ? current.count + 1 : Math.max(0, current.count - 1), reacted};
+                return next;
+            })(),
         }));
 
         try {
             await onToggleReaction(content);
         } catch {
-            // Revert on error
             setLocalReactions((prev) => ({
                 ...prev,
-                [content]: {count: current.count, reacted: current.reacted},
+                [content]: previous,
             }));
         }
-    }, [localReactions, onToggleReaction]);
+    }, [onToggleReaction]);
📝 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
const handleToggle = useCallback(async (content: string) => {
const current = localReactions[content];
const newReacted = !current.reacted;
const newCount = newReacted ? current.count + 1 : Math.max(0, current.count - 1);
// Optimistic update
setLocalReactions((prev) => ({
...prev,
[content]: {count: newCount, reacted: newReacted},
}));
try {
await onToggleReaction(content);
} catch {
// Revert on error
setLocalReactions((prev) => ({
...prev,
[content]: {count: current.count, reacted: current.reacted},
}));
}
}, [localReactions, onToggleReaction]);
const handleToggle = useCallback(async (content: string) => {
let previousState = {count: 0, reacted: false};
// Optimistic update with state capture
setLocalReactions((prev) => {
previousState = prev[content] || {count: 0, reacted: false};
const reacted = !previousState.reacted;
return {
...prev,
[content]: {
count: reacted ? previousState.count + 1 : Math.max(0, previousState.count - 1),
reacted
},
};
});
try {
await onToggleReaction(content);
} catch {
// Revert on error
setLocalReactions((prev) => ({
...prev,
[content]: previousState,
}));
}
}, [onToggleReaction]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/components/sidebar_right/pr_review_detail/reaction_bar.tsx` around
lines 52 - 72, The handler handleToggle currently reads localReactions[content]
from a stale closure; change it to perform the optimistic update using the
functional updater of setLocalReactions (compute current = prev[content] inside
the updater) and remove direct reads of localReactions from the closure and from
the dependency array; to support proper revert on error, capture the previous
snapshot into a ref (e.g., prevReactionsRef) immediately before the optimistic
update and, on catch, restore that snapshot via setLocalReactions(prev =>
prevReactionsRef.current) — update references to handleToggle,
setLocalReactions, localReactions, and onToggleReaction accordingly.

Comment on lines +264 to +270
function prReviewThreadsLoading(state = false, action: {type: string, data: boolean}) {
switch (action.type) {
case ActionTypes.PR_REVIEW_THREADS_LOADING:
return action.data;
default:
return state;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reset review-thread loading state on PR clear.

When clearing the selected PR, prReviewThreadsLoading should also return to false to avoid stale loading indicators.

💡 Proposed fix
 function prReviewThreadsLoading(state = false, action: {type: string, data: boolean}) {
     switch (action.type) {
     case ActionTypes.PR_REVIEW_THREADS_LOADING:
         return action.data;
+    case ActionTypes.CLEAR_SELECTED_PR:
+        return false;
     default:
         return state;
     }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@webapp/src/reducers/index.ts` around lines 264 - 270, prReviewThreadsLoading
currently only responds to ActionTypes.PR_REVIEW_THREADS_LOADING and can stay
true after the selected PR is cleared; update the reducer function
prReviewThreadsLoading to also handle the action type that clears the selected
PR (e.g., ActionTypes.CLEAR_SELECTED_PR) and return false in that case so the
loading flag resets when the PR is cleared.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a comprehensive PR review comments dashboard to the right-hand sidebar (RHS) in Mattermost, enabling users to view, reply to, react to, resolve, and assign review comments to AI agents without leaving Mattermost. The implementation includes both backend GraphQL infrastructure for fetching review threads and frontend React components for displaying and interacting with review comments.

Changes:

  • Added backend GraphQL queries/mutations for review threads with pagination support, plus 6 new API endpoints for comment operations and AI agent configuration
  • Implemented frontend Redux state management and React components for drill-down PR review detail view with file-grouped threads, diff snippets, inline replies, emoji reactions, and AI agent assignment
  • Added AI Agents configuration feature with Claude and Cursor as default agents

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
webapp/src/types/github_types.ts Added TypeScript interfaces for review threads, comments, reactions, AI agents, and PR selection data
webapp/src/selectors.ts Added selectors for PR review state and threads grouped by file
webapp/src/reducers/index.ts Added reducers for selected PR, review threads, loading state, and AI agents
webapp/src/constants/index.js Added REACTIONS constant array for emoji reactions
webapp/src/components/sidebar_right/sidebar_right.jsx Integrated PRReviewDetail view with conditional rendering based on selectedPR
webapp/src/components/sidebar_right/pr_review_detail/*.tsx Implemented 8 new React components for review detail UI including header, file groups, threads, comments, replies, reactions, diff display, and AI assignment
webapp/src/components/sidebar_right/index.jsx Connected new actions and selectors for PR review functionality
webapp/src/components/sidebar_right/github_items.tsx Added PR drill-down navigation with onClick handler
webapp/src/client/client.ts Added 6 new client methods for review threads API endpoints
webapp/src/actions/index.ts Implemented 6 new action creators for PR review operations
webapp/src/action_types/index.ts Added 6 new action type constants
server/plugin/graphql/review_threads_query.go Defined GraphQL query structures for review threads, comments, and resolution mutations
server/plugin/graphql/review_threads.go Implemented GetReviewThreads and ResolveReviewThread methods with pagination
server/plugin/graphql/client.go Added executeMutation method for GraphQL mutations
server/plugin/configuration.go Added AIAgent type and ParseAIAgents configuration method
server/plugin/api.go Implemented 6 new HTTP handlers for PR review endpoints
plugin.json Added AIAgents configuration setting

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +98 to +101
thread := ReviewThread{
ID: string(threadNode.ID),
IsResolved: bool(threadNode.IsResolved),
ResolvedByLogin: string(threadNode.ResolvedBy.Login),
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

When a review thread is not resolved, the ResolvedBy field will be null in the GraphQL response, but converting threadNode.ResolvedBy.Login to a string will result in an empty string rather than handling the null case properly. This could cause issues in the frontend which expects resolved_by to be either an object with a login field or null. Consider checking if ResolvedBy is null before accessing its Login field, or adjust the type handling to properly represent null values.

Suggested change
thread := ReviewThread{
ID: string(threadNode.ID),
IsResolved: bool(threadNode.IsResolved),
ResolvedByLogin: string(threadNode.ResolvedBy.Login),
resolvedByLogin := ""
if threadNode.ResolvedBy != nil {
resolvedByLogin = string(threadNode.ResolvedBy.Login)
}
thread := ReviewThread{
ID: string(threadNode.ID),
IsResolved: bool(threadNode.IsResolved),
ResolvedByLogin: resolvedByLogin,

Copilot uses AI. Check for mistakes.

var reviewThreadsQuery struct {
Repository struct {
PullRequest struct {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Reviews are limited to the first 100 entries without pagination support. For PRs with more than 100 reviews, subsequent reviews will not be counted in the approved/changes_requested summary. Consider implementing pagination for reviews or increasing the limit and documenting this constraint.

Suggested change
PullRequest struct {
PullRequest struct {
// NOTE: GitHub GraphQL limits the maximum page size to 100 items.
// This query intentionally requests only the first 100 reviews.
// Pull requests with more than 100 reviews will have subsequent reviews
// ignored by any summary logic that processes this slice. Supporting
// more than 100 reviews requires adding pagination (e.g., an "after"
// cursor and corresponding PageInfo) and updating the calling code.

Copilot uses AI. Check for mistakes.
actions.getPRReviewThreads(selectedPR.owner, selectedPR.repo, selectedPR.number);
actions.getAIAgents();
}
}, [selectedPR?.owner, selectedPR?.repo, selectedPR?.number]); // eslint-disable-line react-hooks/exhaustive-deps
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The actions object is missing from the dependencies array despite being used in the useEffect. While this is suppressed with an eslint-disable comment, it can lead to stale closure issues if the actions change. Consider including actions in the dependency array, or if actions are stable (memoized via bindActionCreators), document why it's safe to omit them.

Suggested change
}, [selectedPR?.owner, selectedPR?.repo, selectedPR?.number]); // eslint-disable-line react-hooks/exhaustive-deps
}, [selectedPR?.owner, selectedPR?.repo, selectedPR?.number, actions]);

Copilot uses AI. Check for mistakes.
id: string;
database_id: number;
body: string;
author: {login: string; avatar_url: string};
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The frontend type expects an author object with both login and avatar_url fields, but the backend API only returns author_login as a string (see server/plugin/api.go line 1873). This mismatch will cause the avatar display in review comments to fail silently when the component tries to access comment.author.avatar_url. Either update the backend to fetch and return avatar URLs (e.g., by querying the GitHub API or constructing the URL from the login), or change the frontend type to match what the backend actually provides.

Suggested change
author: {login: string; avatar_url: string};
author_login: string;

Copilot uses AI. Check for mistakes.
path: string;
line: number;
start_line: number;
reactions: {content: string; count: number; reacted: boolean}[];
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The frontend expects a reacted boolean field in reactions to indicate if the current user has reacted, but the GraphQL query only fetches ReactionGroups which provide aggregated counts without per-user information. This will cause the reaction buttons to not properly show the current user's reaction state. The GraphQL query should include ViewerHasReacted information for each reaction group, or the backend should query reactions separately to determine if the current user has reacted to each comment.

Suggested change
reactions: {content: string; count: number; reacted: boolean}[];
reactions: {content: string; count: number; reacted?: boolean}[];

Copilot uses AI. Check for mistakes.

const handleResolveToggle = useCallback(async () => {
const action = isResolved ? 'unresolve' : 'resolve';
return resolveThread(thread.id, action);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

Similar to the reply functionality, if resolving/unresolving a thread fails, there's no error feedback to the user. The button click happens but the UI state doesn't change (because the data isn't refetched), leaving the user confused. Add error handling and user feedback for failed resolve/unresolve operations.

Suggested change
return resolveThread(thread.id, action);
try {
await resolveThread(thread.id, action);
} catch (error) {
// Provide basic feedback so the user isn't left confused when the UI doesn't change.
// eslint-disable-next-line no-console
console.error('Failed to resolve/unresolve review thread:', error);
window.alert('Failed to update the thread resolution state. Please try again.');
}

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +20
const defaultMention = agents.length > 0 ? (agents.find((a) => a.is_default)?.mention || agents[0].mention) : '';
const [selectedAgent, setSelectedAgent] = useState(defaultMention);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The selectedAgent state is initialized with defaultMention only once when the component mounts. If the agents list changes (e.g., from empty to populated after loading), the selectedAgent won't update to reflect the new default. Consider using useEffect to update selectedAgent when agents changes, or move the default selection logic into the render to always compute the current default.

Copilot uses AI. Check for mistakes.
if err != nil {
p.writeAPIError(w, &APIErrorResponse{Message: "Invalid param 'number'.", StatusCode: http.StatusBadRequest})
return
}
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The parameter validation allows a zero or negative PR number, which would lead to invalid GitHub API calls. Add validation to ensure the number is positive (greater than 0) before proceeding with the GraphQL query.

Suggested change
}
}
if number <= 0 {
p.writeAPIError(w, &APIErrorResponse{Message: "Invalid param 'number'. Must be greater than 0.", StatusCode: http.StatusBadRequest})
return
}

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +96
<div
style={{
...styles.resolvedCollapsed,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03),
borderLeft: `3px solid ${changeOpacity(theme.centerChannelColor, 0.2)}`,
}}
onClick={() => setExpandedResolved(true)}
>
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
<input
type='checkbox'
checked={isSelected}
onChange={(e) => {
e.stopPropagation();
handleCheckboxChange();
}}
onClick={(e) => e.stopPropagation()}
style={styles.checkbox}
/>
<span style={{...styles.resolvedLabel, color: changeOpacity(theme.centerChannelColor, 0.5)}}>
{'Resolved thread'}
{thread.resolved_by ? ` by ${thread.resolved_by.login}` : ''}
{' - '}
{firstComment?.body ? firstComment.body.substring(0, 80) + (firstComment.body.length > 80 ? '...' : '') : ''}
</span>
</div>
<span style={{...styles.expandHint, color: changeOpacity(theme.centerChannelColor, 0.4)}}>
{'Click to expand'}
</span>
</div>
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

The clickable collapsed thread container (line 67-96) lacks keyboard accessibility. Users navigating with keyboard cannot expand resolved threads. Add appropriate ARIA attributes (e.g., role="button", tabIndex="0") and keyboard event handlers (onKeyDown for Enter/Space keys) to make this interactive element accessible.

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +43
const handleSubmit = useCallback(async () => {
const body = text.trim();
if (!body || submitting) {
return;
}

setSubmitting(true);
try {
await onSubmit(body);
setText('');
setExpanded(false);
} finally {
setSubmitting(false);
}
}, [text, submitting, onSubmit]);
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

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

If the reply submission fails (e.g., network error, API error), the error is silently swallowed and the user receives no feedback. The text area collapses and clears as if the submission succeeded. Add error handling to display an error message to the user when submission fails, and keep the text in the textarea so they can retry.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Do Not Merge Should not be merged until this label is removed Setup Cloud Test Server Setup a test server using Mattermost Cloud

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants