Skip to content

Implement Mattermost plugin for Cursor Background Agents - #1

Merged
nickmisasi merged 8 commits into
masterfrom
first-run
Feb 13, 2026
Merged

Implement Mattermost plugin for Cursor Background Agents#1
nickmisasi merged 8 commits into
masterfrom
first-run

Conversation

@nickmisasi

@nickmisasi nickmisasi commented Feb 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

Complete v1 implementation of the Mattermost plugin for Cursor Background Agents, mirroring the Cursor Slack integration feature-for-feature.

  • Server (Go): Cursor API client, @cursor mention handling, message parser, background status poller, thread follow-ups with LLM-enriched context via bridge client, slash commands (list/status/cancel/settings/models/help), interactive settings dialog, GitHub webhook receiver with HMAC-SHA256 verification, PR lifecycle notifications, admin health endpoint, toggleable debug logging, configurable agent system prompt
  • Webapp (React/TypeScript): Redux store, RHS agent dashboard, App Bar icon, post dropdown menu actions, WebSocket real-time updates, Mattermost CSS variable styling with dark mode support
  • Documentation: CLAUDE.md files throughout the repo for progressive context disclosure, 8 developer skills in .claude/commands/ for common tasks

Built across 6 phases, each with dedicated planning, implementation, code review, and remediation. QA-validated end-to-end on a live Mattermost server.

Test plan

  • All Go tests pass (go test ./server/... — 5 packages)
  • All webapp tests pass (npm test — 14 tests, 3 suites)
  • go vet clean
  • Server and webapp builds clean
  • Plugin deploys and activates successfully
  • Bot @cursor account created
  • /cursor help, /cursor list, /cursor settings verified working
  • RHS panel opens via App Bar icon
  • System Console settings page renders all 8 fields
  • Debug logging toggleable from System Console
  • Error messages render as formatted JSON code blocks

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Full /cursor slash command (launch, list, status, cancel, models, settings, help) with follow-up and cancel flows
    • RHS UI: agent list, detail view, status badges, follow-up input, and PR/Cursor links
    • REST API + WebSocket events for agents enabling live updates
    • GitHub webhook notifications for PRs and reviews
    • Admin health check and expanded plugin settings
  • Documentation

    • Detailed developer & user guides for APIs, KV store, parser, deployment, debugging, and testing
  • Tests

    • Extensive unit/integration tests across commands, handlers, API, poller, webhook, dialog, store, and client

Complete v1 implementation mirroring Cursor's Slack integration for
Mattermost. Built across 6 phases with full code review and QA validation.

Server (Go):
- Cursor API client with retry, Basic Auth, all 9 endpoints
- Message parser for @cursor mentions (repo, branch, model, options)
- MessageHasBeenPosted hook for mention-driven agent launching
- HA-safe background poller via cluster.Schedule
- Thread follow-up detection and Cursor API forwarding
- Thread context enrichment via Agents plugin bridge client
- 7 slash commands: launch, list, status, cancel, settings, models, help
- Interactive settings dialog for channel/user defaults
- GitHub webhook receiver with HMAC-SHA256 verification
- PR lifecycle notifications (merged, closed, reviewed)
- Admin health endpoint with rich diagnostics
- Toggleable debug logging for API troubleshooting
- Configurable system prompt for Cursor agents

Webapp (React/TypeScript):
- Redux store with WebSocket-driven real-time updates
- RHS agent dashboard with list and detail views
- App Bar icon with RHS toggle
- Post dropdown menu actions (follow-up, cancel, view details)
- Mattermost CSS variables throughout (dark mode compatible)

Documentation:
- CLAUDE.md files for progressive context disclosure
- 8 skills in .claude/commands/ for common development tasks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR converts the starter template into a Cursor Background Agents plugin: adds a Cursor API client and types, KV-backed store and indices, GitHub webhook handling, slash command and mention-driven agent flows, REST API and RHS UI with WebSocket events, background polling, extensive tests, and comprehensive documentation.

Changes

Cohort / File(s) Summary
Docs & Guides
\.claude/commands/*, CLAUDE.md, server/CLAUDE.md, server/cursor/CLAUDE.md, server/parser/CLAUDE.md, server/store/CLAUDE.md, webapp/CLAUDE.md
Added many design and how‑to documents (end‑to‑end guides, checklists) for API endpoints, KV entities, parser, Cursor client, webapp wiring, deploy/debug, and features.
Module & Manifest
go.mod, plugin.json, Makefile, .gitignore
Module path and dependency updates, expanded plugin.json settings schema, mockgen path adjusted in Makefile, and .qa/ added to .gitignore.
Core plugin init & wiring
server/plugin.go, server/plugin_test.go
OnActivate/OnDeactivate wiring, Cursor client and bridge init, KV store, bot account setup, ServeHTTP router, command handler, background poller; added concurrency-safe accessors and debug logging; tests updated.
Configuration
server/configuration.go
Structured configuration with public fields, validation (IsValid), defaults, GetPollInterval, and async Cursor API key validation.
REST API & auth
server/api.go, server/api_test.go
New /api/v1 endpoints (agents list/get, followup, cancel), admin health route, auth/admin middleware, health check implementation and types, plus comprehensive tests.
Slash command subsystem
server/command/...
server/command/command.go, server/command/command_test.go, server/command/mocks/*
Full /cursor command implementation (subcommands, autocomplete, launch/list/status/cancel/settings/models/help), new command types and constructor, extensive mock-based tests; removed old generated mock file.
Message handlers & parser
server/handlers.go, server/parser/parser.go, server/parser/parser_test.go
Mention parser supporting bracketed/inline/natural forms, thread enrichment, launch/follow‑up flows, prompt wrapping and related unit tests.
Cursor client & types
server/cursor/client.go, server/cursor/types.go, server/cursor/client_test.go
New Cursor client interface/implementation (Basic Auth, retries/backoff, APIError), agent/types, logger option, constructors, and HTTP tests.
KV store interface & implementation
server/store/kvstore/kvstore.go, server/store/kvstore/store.go, server/store/kvstore/store_test.go
New AgentRecord, channel/user settings types, expanded KVStore interface, concrete store with active/user/pr/branch/thread indices, idempotency keys, normalization, and tests; removed legacy startertemplate wrapper.
Poller & lifecycle handlers
server/poller.go, server/poller_test.go
Periodic poller for agent statuses, status-specific handlers (running/finished/failed/stopped), post updates, reaction swaps, post props updates, WebSocket events, and tests.
GitHub webhook integration
server/webhook.go, server/webhook_test.go
HMAC signature verification, delivery idempotency, PR/review event parsing and routing, agent lookup by PR/branch, thread notifications, reaction updates, and tests.
Dialog / settings endpoint
server/dialog.go, server/dialog_test.go
Settings dialog submission handler with validation, KV persistence, and tests.
Removed / replaced files
server/command/mocks/mock_commands.go, server/job.go, server/store/kvstore/startertemplate.go
Deleted generated mock and placeholder job method; removed old KV wrapper replaced by new store implementation.
Webapp frontend
webapp/src/*, webapp/package.json
Typed REST client, Redux actions/thunks/reducer/selectors, WebSocket handlers, RHS components (list/card/detail/panel), common components/styles, post dropdown actions, tests, and Jest config updates.
Tests & coverage
server/*_test.go, webapp/src/reducer.test.ts
Extensive unit/integration tests added across API, commands, handlers, poller, webhook, KV store, cursor client, dialog, parser, and frontend reducer; many new test files require review.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Mattermost
    participant Plugin
    participant Parser
    participant CursorAPI
    participant KVStore
    participant Bot

    User->>Mattermost: Post message with `@cursor` ...
    Mattermost->>Plugin: MessageHasBeenPosted
    Plugin->>Parser: Parse(message, botMention)
    Parser-->>Plugin: ParsedMention
    Plugin->>Plugin: Resolve defaults & enrich prompt
    Plugin->>CursorAPI: LaunchAgent(request)
    CursorAPI-->>Plugin: Agent{ID, status=CREATING}
    Plugin->>KVStore: SaveAgent(record)
    KVStore-->>Plugin: OK
    Plugin->>Bot: Post agent link in thread
    Bot-->>Mattermost: Thread message posted
    Plugin->>Mattermost: Publish WebSocket event (agent_created)
Loading
sequenceDiagram
    participant Poller
    participant KVStore
    participant CursorAPI
    participant Plugin
    participant Bot
    participant WebSocket

    loop periodic poll
        Poller->>KVStore: ListActiveAgents()
        KVStore-->>Poller: [AgentRecords]
        Poller->>CursorAPI: GetAgent(agent_id)
        CursorAPI-->>Poller: Agent{status, summary, pr_url}
        Poller->>Plugin: handleAgentFinished(...) (or other)
        Plugin->>KVStore: SaveAgent(updated)
        KVStore-->>Plugin: OK
        Plugin->>Bot: Post completion in thread
        Plugin->>WebSocket: Publish agent_status_changed
    end
Loading
sequenceDiagram
    participant GitHub
    participant Plugin
    participant KVStore
    participant CursorAPI
    participant Bot
    participant Mattermost

    GitHub->>Plugin: POST webhook (pull_request/review)
    Plugin->>Plugin: verifyWebhookSignature()
    Plugin->>KVStore: GetAgentByPRURL or GetAgentByBranch
    KVStore-->>Plugin: AgentRecord
    Plugin->>CursorAPI: GetAgent(id)
    CursorAPI-->>Plugin: Agent{status,summary}
    Plugin->>KVStore: SaveAgent(updated)
    Plugin->>Bot: Post notification to thread
    Plugin->>Mattermost: Swap reactions on trigger post
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Poem

🐇 I hopped from template into Cursor delight,
I stitched webhooks, polls, and threads through the night.
Clients, KV indices, RHS in a row,
Tests and docs bundled in one tidy flow.
A carrot of features — now watch agents go!

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Implement Mattermost plugin for Cursor Background Agents' clearly and directly describes the primary change: a complete implementation of a new Mattermost plugin.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into master

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch first-run

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In @.claude/commands/debug-cursor-api.md:
- Around line 175-219: Replace literal basic-auth placeholders (-u
"YOUR_API_KEY:") with an environment-variable pattern to avoid secret-scanning;
export a variable (e.g., CURSOR_API_KEY) and use curl -u "$CURSOR_API_KEY:" in
every example (Test API Key, List Agents, Get Agent Status, Launch Agent, List
Models, Stop Agent) so update all occurrences of the string YOUR_API_KEY and the
-u argument in the markdown examples to reference the env var instead.

In `@server/api.go`:
- Around line 368-377: The code currently proceeds to mark agents STOPPED even
when the remote Cursor client is unavailable; update the guard around
p.getCursorClient() so that if cursorClient == nil you log a clear error via
p.API.LogError (include "agentID" and context), send an appropriate http error
(e.g. http.StatusBadGateway) and return early instead of continuing to update
local state; ensure the only place that calls cursorClient.StopAgent(ctx,
agentID) remains inside the non-nil branch and that any subsequent local state
update (where you set the agent to STOPPED) is not executed unless StopAgent
succeeded.

In `@server/command/command_test.go`:
- Around line 834-853: There are two conflicting implementations of
sanitizeBranchName (one used in command.go and one in handlers.go) with
different truncation limits and prefix behavior; consolidate them into a single
canonical function sanitizeBranchName that takes an optional maxLen (or a
constant) and a boolean flag for whether to include the "cursor/" prefix, then
replace both existing copies with calls to this single implementation (or export
a wrapper like sanitizeBranchNameWithPrefix) so truncation limit and prefixing
are consistent across usages (ensure tests are updated to use the chosen max
length and prefix semantics and remove the duplicate implementation).

In `@server/command/command.go`:
- Around line 218-227: The SaveAgent call in the /cursor command path
(h.deps.Store.SaveAgent with kvstore.AgentRecord) is missing Prompt, Model,
TargetBranch, CreatedAt and UpdatedAt, causing incomplete records; update the
AgentRecord construction used when saving (the call around agent, botPost, args,
repo, branch) to include Prompt (agent.Prompt or args-provided prompt), Model
(agent.Model), TargetBranch (branch or agent.TargetBranch), and set
CreatedAt/UpdatedAt to the current timestamp (matching the pattern used in
handlers.go where those fields are populated) so command-launched agents persist
the same fields as the `@mention` path.

In `@server/cursor/client.go`:
- Around line 112-244: The doRequest function currently retries all HTTP methods
on 429/5xx which can cause duplicate non-idempotent POST operations (affecting
LaunchAgent, AddFollowup, StopAgent); change the retry logic in doRequest to
only perform automatic retries for idempotent methods (e.g., GET, DELETE, HEAD,
OPTIONS, and optionally PUT) by checking the request method before treating
429/5xx as retryable, and for non-idempotent methods (POST, PATCH) return the
API error immediately instead of continuing the loop; update the retry-related
log messages to reflect the method-based decision and add/adjust tests for
LaunchAgent/AddFollowup/StopAgent to ensure POSTs are not retried by doRequest.

In `@server/handlers.go`:
- Around line 695-706: sanitizeBranchName can return "cursor/" when the slug is
empty (prompt was only non-alphanumerics), which is invalid; update the
sanitizeBranchName function to detect when the post-processed slug is empty and
replace it with a safe fallback (for example "untitled" or a short unique token
like "branch-<timestamp>" or a short hash of the original prompt) before
prefixing with "cursor/"; ensure the fallback is alphanumeric/hyphen-only and
still trimmed/validated so the function always returns a valid branch name.

In `@server/plugin.go`:
- Around line 168-176: The command handler (p.commandHandler) is initialized
with a snapshot of p.cursorClient via
command.NewHandler(command.Dependencies{...}) and therefore keeps a stale client
after setCursorClient/OnConfigurationChange updates p.cursorClient; reinitialize
p.commandHandler whenever the cursor client is changed (e.g., in
OnConfigurationChange after calling setCursorClient()) by recreating it with the
updated p.cursorClient, or refactor the handler to call back to p.cursorClient
dynamically instead of storing the client at construction (refer to
p.commandHandler, command.NewHandler, command.Dependencies, setCursorClient, and
OnConfigurationChange).

In `@server/webhook.go`:
- Around line 252-257: Don't write non-Cursor strings into AgentRecord.Status;
instead add a separate field on AgentRecord (e.g., PRState or PRLifecycle) and
set that to "MERGED" or "PR_CLOSED" from event.PullRequest.Merged, and then set
AgentRecord.Status to a valid Cursor terminal value (use your existing
cursor-normalization helper or map to the closest Cursor enum) so UI rendering,
status→emoji mapping and IsTerminal checks continue to work; update code
references that read PR lifecycle to use the new PRState field.
- Around line 153-183: The code marks deliveries processed unconditionally which
can drop events if handlers fail; change the flow so
MarkDeliveryProcessed(deliveryID) is called only after a successful handler
response: either (a) modify handlers handlePingEvent, handlePullRequestEvent,
handlePullRequestReviewEvent to return an error or status code and call
MarkDeliveryProcessed only on success, or (b) wrap http.ResponseWriter with a
small recorder to capture the final status code after calling the handler and
call MarkDeliveryProcessed only when the recorded status is 2xx; also keep using
HasDeliveryBeenProcessed to skip duplicates before handling.

In `@webapp/src/components/common/styles.css`:
- Around line 16-197: The file uses legacy rgba() notation in several CSS
classes (e.g., .cursor-status-finished, .cursor-status-stopped,
.cursor-rhs-empty, .cursor-rhs-empty-hint, .cursor-agent-card,
.cursor-agent-card:hover, .cursor-agent-card-time, .cursor-agent-card-prompt,
.cursor-agent-detail-label, .cursor-agent-detail-followup,
.cursor-agent-detail-cancel, .cursor-textarea::placeholder); replace each
rgba(var(--center-channel-color-rgb), alpha) with the modern slash syntax
rgb(var(--center-channel-color-rgb) / alpha) (and similarly for other rgba uses
like rgba(var(--center-channel-color-rgb), 0.04) →
rgb(var(--center-channel-color-rgb) / 0.04) and
rgba(var(--center-channel-color-rgb), 0.56) →
rgb(var(--center-channel-color-rgb) / 0.56)) so the styles conform to the
color-function-notation:"modern" rule while keeping the same variables and alpha
values.
🟡 Minor comments (12)
CLAUDE.md-22-47 (1)

22-47: ⚠️ Potential issue | 🟡 Minor

Add a language identifier to the fenced block to satisfy MD040.

🔧 Minimal markdownlint fix
-```
+```text
 server/
   plugin.go          # Plugin struct, OnActivate, ServeHTTP, ExecuteCommand
   configuration.go   # Config struct, OnConfigurationChange, validation
   handlers.go        # MessageHasBeenPosted, agent launch flow, follow-ups, thread enrichment
   api.go             # HTTP router (gorilla/mux), REST endpoints, middleware
   poller.go          # Background agent status polling via cluster.Schedule
   dialog.go          # Settings dialog submission handler
   webhook.go         # GitHub webhook receiver (HMAC verification, PR events)
   command/command.go  # /cursor slash command handler (list, status, cancel, settings, models, help)
   cursor/client.go   # Cursor API HTTP client (interface-based)
   cursor/types.go    # Cursor API request/response types
   parser/parser.go   # `@cursor` mention parser (repo, branch, model, prompt extraction)
   store/kvstore/     # KV store interface + implementation

 webapp/src/
   index.tsx           # Plugin registration (reducer, RHS, app bar, post actions, WS handlers)
   reducer.ts          # Redux reducer for plugin state
   actions.ts          # Action creators (sync + thunks)
   selectors.ts        # Redux selectors
   types.ts            # TypeScript interfaces
   client.ts           # REST client for plugin API
   websocket.ts        # WebSocket event handler registration
   components/rhs/     # RHS panel components (RHSPanel, AgentList, AgentCard, AgentDetail)
   components/common/  # StatusBadge, styles.css
server/parser/parser.go-3-6 (1)

3-6: ⚠️ Potential issue | 🟡 Minor

Invalid autopr values silently disable AutoPR. Consider only setting AutoPR when the value parses cleanly so typos don’t override defaults.

🔧 Suggested change
 import (
 	"regexp"
+	"strconv"
 	"strings"
 )
@@
 	case "autopr":
-		b := strings.EqualFold(value, "true")
-		result.AutoPR = &b
+		if b, err := strconv.ParseBool(value); err == nil {
+			result.AutoPR = &b
+		}
 	}
 }

Also applies to: 147-159

server/parser/CLAUDE.md-22-49 (1)

22-49: ⚠️ Potential issue | 🟡 Minor

Add language tags to fenced examples. This satisfies markdownlint and keeps rendering consistent.

✏️ Example change (apply similarly to other plain-text fences)
-```
+```text
 `@cursor` in backend-api, fix the auth issue      -> Repository: "backend-api"
 `@cursor` in org/repo, fix the auth issue          -> Repository: "org/repo"
 `@cursor` with opus, fix the login bug             -> Model: "opus"
 `@cursor` in org/repo, with opus, fix it           -> Repository + Model
-```
+```
.claude/commands/modify-agent-prompt.md-7-31 (1)

7-31: ⚠️ Potential issue | 🟡 Minor

Add a language tag to the pipeline code fence. This satisfies markdownlint and improves rendering consistency.

✏️ Suggested change
-```
+```text
 User Message
     |
     v
 Parser (server/parser/parser.go)
@@
 Cursor API LaunchAgent Request
     |-- prompt.text = wrapped prompt
     |-- prompt.images = extracted thread images
-```
+```
server/dialog.go-16-34 (1)

16-34: ⚠️ Potential issue | 🟡 Minor

Add authentication check against Mattermost-User-ID header to match request.UserId for defense-in-depth. The endpoint is already protected by MattermostAuthorizationRequired middleware that validates the Mattermost-User-ID header. However, the handler should additionally verify that the authenticated user matches the request's UserId to prevent any potential confusion between the authenticated identity and the request payload. While the current check against request.State provides protection (since State is server-controlled), comparing against the authenticated user from the header is a security best practice.

Suggested check
 func (p *Plugin) handleSettingsDialogSubmission(w http.ResponseWriter, r *http.Request) {
+	authUserID := r.Header.Get("Mattermost-User-ID")
+	if authUserID == "" {
+		http.Error(w, "unauthorized", http.StatusUnauthorized)
+		return
+	}
+
 	var request model.SubmitDialogRequest
 	if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
 		http.Error(w, "invalid request", http.StatusBadRequest)
 		return
 	}

 	parts := strings.SplitN(request.State, "|", 2)
 	if len(parts) != 2 {
 		http.Error(w, "invalid state", http.StatusBadRequest)
 		return
 	}
 	channelID := parts[0]
 	userID := parts[1]

-	if request.UserId != userID {
+	if request.UserId != userID || request.UserId != authUserID {
 		http.Error(w, "unauthorized", http.StatusForbidden)
 		return
 	}
webapp/src/components/rhs/AgentDetail.tsx-96-105 (1)

96-105: ⚠️ Potential issue | 🟡 Minor

Use a semantic label for the follow-up textarea.

This improves screen reader association and form accessibility.

♿ Suggested accessibility tweak
-            {agent.status === 'RUNNING' && (
+            {agent.status === 'RUNNING' && (
                 <div className='cursor-agent-detail-followup'>
-                    <div className='cursor-agent-detail-label'>{'Send Follow-up'}</div>
+                    <label
+                        className='cursor-agent-detail-label'
+                        htmlFor='cursor-followup-textarea'
+                    >
+                        {'Send Follow-up'}
+                    </label>
                     <textarea
+                        id='cursor-followup-textarea'
                         className='cursor-textarea'
                         placeholder='Enter follow-up instructions...'
                         value={followupText}
                         onChange={(e) => setFollowupText(e.target.value)}
                         rows={3}
                     />
webapp/src/components/common/StatusBadge.tsx-17-19 (1)

17-19: ⚠️ Potential issue | 🟡 Minor

Add accessible label for the status indicator.
The status badge is color-only, particularly in the agent card view where status text isn't adjacent. Add an aria-label to ensure screen reader users can identify the agent status.

♿ Suggested accessibility fix
-    return <span className={`cursor-status-badge ${className}`} title={status}/>;
+    return (
+        <span
+            className={`cursor-status-badge ${className}`}
+            title={status}
+            aria-label={`Agent status: ${status}`}
+            role='img'
+        />
+    );
plugin.json-57-91 (1)

57-91: ⚠️ Potential issue | 🟡 Minor

Change settings_schema default values from strings to JSON literals.
Mattermost settings_schema expects defaults to match the native JSON type: bool fields require boolean literals (true/false), and number fields require numeric literals. String defaults may not apply correctly. Official Mattermost plugins (GitHub, Calls) use JSON literals for these types.

🔧 Required changes
             {
                 "key": "AutoCreatePR",
                 "display_name": "Auto-Create Pull Requests",
                 "type": "bool",
                 "help_text": "When enabled, agents automatically create pull requests on completion. Users can override per-message with autopr=false.",
-                "default": "true"
+                "default": true
             },
             {
                 "key": "PollIntervalSeconds",
                 "display_name": "Status Poll Interval (seconds)",
                 "type": "number",
                 "help_text": "How often the plugin checks for agent status changes. Minimum 10 seconds. Lower values increase API usage.",
-                "default": "30",
+                "default": 30,
                 "placeholder": "30"
             },
             {
                 "key": "EnableDebugLogging",
                 "display_name": "Enable Debug Logging",
                 "type": "bool",
                 "help_text": "When enabled, the plugin logs detailed debug information including API request/response bodies. Disable in production to reduce log noise.",
-                "default": "false"
+                "default": false
             }
webapp/src/reducer.ts-21-26 (1)

21-26: ⚠️ Potential issue | 🟡 Minor

Clear stale selection after full agent refresh.

When the agents map is rebuilt, a previously selected ID might no longer exist, which can leave selectors with undefined data. Consider nulling the selection if it’s not present in the new map.

🔧 Suggested fix
 case AGENTS_RECEIVED: {
     const agents: Record<string, Agent> = {};
     for (const agent of action.data) {
         agents[agent.id] = agent;
     }
-    return {...state, agents};
+    const selectedAgentId =
+        state.selectedAgentId && agents[state.selectedAgentId] ? state.selectedAgentId : null;
+    return {...state, agents, selectedAgentId};
 }
webapp/src/components/rhs/AgentCard.tsx-28-66 (1)

28-66: ⚠️ Potential issue | 🟡 Minor

Add Space-key activation for keyboard accessibility.

With role="button", keyboard users expect both Space and Enter to activate the button. Currently only Enter is handled, violating WAI-ARIA Button Pattern requirements.

♿ Suggested fix
         onKeyDown={(e) => {
-            if (e.key === 'Enter') {
-                onClick();
-            }
+            if (e.key === 'Enter' || e.key === ' ') {
+                e.preventDefault();
+                onClick();
+            }
         }}
.claude/commands/add-webhook-event.md-97-104 (1)

97-104: ⚠️ Potential issue | 🟡 Minor

Example code references undefined field.

The handler example accesses event.PullRequest on line 99, but the CheckRunEvent struct defined in Step 2 (lines 38-48) doesn't include a PullRequest field. This would cause a compile error if developers follow the example literally.

For check_run events, the PR information is typically nested under check_run.pull_requests (an array). Either update the struct definition to include the relevant fields, or adjust the lookup strategy in the example.

Suggested fix for the struct definition
 type CheckRunEvent struct {
     Action   string `json:"action"`
     CheckRun struct {
         Name       string `json:"name"`
         Status     string `json:"status"`
         Conclusion string `json:"conclusion"`
         HTMLURL    string `json:"html_url"`
+        PullRequests []struct {
+            URL string `json:"url"`
+        } `json:"pull_requests"`
     } `json:"check_run"`
     Repository ghRepository `json:"repository"`
     Sender     ghSender     `json:"sender"`
 }

Then update the lookup to iterate over event.CheckRun.PullRequests or look up by branch instead.

server/handlers.go-353-366 (1)

353-366: ⚠️ Potential issue | 🟡 Minor

Remove the eyes reaction when the follow-up text is empty.

Right now sendFollowUp returns early and leaves the “eyes” reaction in place, which reads like an acknowledged follow-up even though nothing was sent.

🧹 Suggested fix
     followUpText = strings.TrimSpace(followUpText)

     if followUpText == "" {
+        p.removeReaction(post.Id, "eyes")
         return
     }
🧹 Nitpick comments (10)
.claude/commands/add-rhs-feature.md (1)

7-23: Consider referencing STYLE_GUIDE.md for UI consistency.

📌 Suggested addition
 Key files:
 - `webapp/src/index.tsx` -- Plugin registration (reducer, RHS, app bar, post actions, websocket)
 - `webapp/src/reducer.ts` -- Redux reducer
 - `webapp/src/actions.ts` -- Action type constants, action interfaces, sync/async action creators
 - `webapp/src/selectors.ts` -- Redux selectors
 - `webapp/src/types.ts` -- TypeScript interfaces
 - `webapp/src/client.ts` -- HTTP client for plugin API calls
 - `webapp/src/websocket.ts` -- WebSocket event handler registration
 - `webapp/src/components/rhs/` -- RHS panel components
 - `webapp/src/components/common/` -- Shared components and CSS
+ - `STYLE_GUIDE.md` -- Colors, typography, spacing, component patterns, motion guidelines
Based on learnings: "Reference `STYLE_GUIDE.md` for colors, typography, spacing, component patterns, and motion guidelines when building new UI components."
webapp/src/components/rhs/RHSPanel.tsx (1)

11-19: Create a typed dispatch hook to eliminate as any casting. Define a useAppDispatch hook that properly types the dispatch to accept thunks, removing the need for type assertions throughout the codebase. This would improve type safety across RHSPanel and other components that dispatch async actions.

webapp/src/index.tsx (1)

13-14: Consider using more specific types for action references.

The object type for rhsToggleAction and rhsShowAction is very permissive. Based on the registerRightHandSidebarComponent return type in the type definitions, these could be typed more specifically, though object is acceptable given the opaque nature of Redux actions.

server/cursor/client_test.go (1)

350-364: Potential test resource leak on assertion failure.

The test server handler uses select {} which blocks forever. If the test assertions fail before the context cancellation propagates, the goroutine will leak until the test process exits. This is generally acceptable for tests but worth noting.

Consider adding a timeout to the context or the select to make debugging easier if this test becomes flaky:

select {
case <-r.Context().Done():
    return
case <-time.After(5 * time.Second):
    t.Error("handler was not cancelled")
}
server/command/command_test.go (1)

19-193: Duplicate mock implementations across test files.

The mockCursorClient and mockKVStore implementations are duplicated from server/handlers_test.go. Consider extracting these into a shared testutil or mocks package to reduce duplication and ensure consistency.

Suggested approach

Create a shared test utilities package:

server/
  testutil/
    mocks.go  // Contains mockCursorClient, mockKVStore

Then import in both test files:

import "github.com/mattermost/mattermost-plugin-cursor/server/testutil"

This ensures mock implementations stay in sync and reduces maintenance burden.

server/store/kvstore/store.go (3)

71-85: Silent failures in index updates could cause data inconsistency.

The secondary index updates for user, PR URL, and branch all ignore errors with _, _. While this prevents index failures from blocking the primary save, it could lead to data inconsistency where lookups by these indices fail to find records that exist.

Consider at minimum logging these errors for debugging:

Suggested improvement
 // Maintain a per-user agent index for GetAgentsByUser.
 if record.UserID != "" {
     key := prefixUserAgentIdx + record.UserID + ":" + record.CursorAgentID
-    _, _ = s.client.KV.Set(key, record.CursorAgentID)
+    if _, err := s.client.KV.Set(key, record.CursorAgentID); err != nil {
+        // Log but don't fail - index is secondary
+        _ = err // TODO: Add logging when logger is available
+    }
 }

Alternatively, if index consistency is critical, consider returning a multi-error that captures all failures.


107-126: ListKeys has a hardcoded limit of 1000.

The GetAgentsByUser and ListActiveAgents functions use a hardcoded limit of 1000 keys. For deployments with many agents, this could silently truncate results.

Consider either:

  1. Documenting this limitation
  2. Implementing pagination
  3. Making the limit configurable

197-204: GetChannelSettings returns empty struct when not found.

When the channel settings key doesn't exist, GetChannelSettings returns an empty ChannelSettings{} struct rather than nil. This differs from GetAgent which returns nil when not found.

This inconsistency could cause confusion - callers might check settings.DefaultRepository == "" to detect "not found" vs "found but empty". Consider aligning the behavior with GetAgent:

Suggested fix for consistency
 func (s *store) GetChannelSettings(channelID string) (*ChannelSettings, error) {
     var settings ChannelSettings
     err := s.client.KV.Get(prefixChannel+channelID, &settings)
     if err != nil {
         return nil, errors.Wrap(err, "failed to get channel settings")
     }
+    // Return nil if not found (all fields are zero values)
+    if settings.DefaultRepository == "" && settings.DefaultBranch == "" {
+        return nil, nil
+    }
     return &settings, nil
 }

Apply the same pattern to GetUserSettings.

server/handlers.go (1)

544-613: Consider caching display names per thread to reduce API calls.

getDisplayName can be invoked for every post; a simple per-thread cache avoids repeated GetUser lookups for the same author.

♻️ Optional caching tweak
 func (p *Plugin) formatThread(postList *model.PostList) (string, []cursor.Image) {
     order := postList.Order
     posts := postList.Posts
+    displayNameCache := map[string]string{}

     // Sort by CreateAt for chronological order.
     sort.Slice(order, func(i, j int) bool {
         return posts[order[i]].CreateAt < posts[order[j]].CreateAt
     })

@@
         threadPost := posts[postID]

         // Get display name for the author.
-        displayName := p.getDisplayName(threadPost.UserId)
+        displayName, ok := displayNameCache[threadPost.UserId]
+        if !ok {
+            displayName = p.getDisplayName(threadPost.UserId)
+            displayNameCache[threadPost.UserId] = displayName
+        }
         sb.WriteString(fmt.Sprintf("[%s]: %s\n", displayName, threadPost.Message))
server/api.go (1)

204-222: Remove unused RootPostID or populate it if needed.

AgentResponse includes RootPostID, but it's never set in API responses and the webapp doesn't use it—it only appears in type definitions and test initialization where it's set to empty string. Either remove the field or populate it with a.PostID / record.PostID if intended for future use.

Comment thread .claude/commands/debug-cursor-api.md Outdated
Comment thread server/api.go
Comment thread server/command/command_test.go
Comment thread server/command/command.go
Comment thread server/cursor/client.go
Comment thread server/handlers.go
Comment thread server/plugin.go
Comment thread server/webhook.go
Comment thread server/webhook.go
Comment thread webapp/src/components/common/styles.css
- Fix import ordering across all files (import/order)
- Replace raw <a target="_blank"> with ExternalLink component
- Fix operator-linebreak, dot-notation, quote-props, naming-convention
- Remove unused _dispatch parameters from thunks
- Fix jsx-max-props-per-line in StatusBadge

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@webapp/src/actions.ts`:
- Around line 120-129: The websocketAgentStatusChange action creator currently
uses parseInt(data.updated_at, 10) which can return NaN for invalid timestamp
strings; update websocketAgentStatusChange to validate the parsed value (e.g.,
const ts = parseInt(...); if (Number.isNaN(ts)) use a safe fallback like
Date.now() or 0) and assign that validated/fallback value to updated_at, and
apply the same validation/fallback pattern to the other similar handlers
referenced in the file (the other action creators around lines 131-149) so
invalid timestamps never enter Redux state.

In `@webapp/src/components/rhs/AgentCard.tsx`:
- Around line 42-46: The onKeyDown handler in AgentCard only triggers on Enter,
so update the onKeyDown in the AgentCard component to also detect Space (handle
e.key === ' ' and legacy values like 'Spacebar' or 'Space') and call onClick()
when Space is pressed; also call e.preventDefault() when handling Space to stop
page scrolling and ensure the element with role='button' is fully keyboard
accessible.
🧹 Nitpick comments (3)
webapp/src/actions.ts (1)

108-116: Consider dispatching state update after successful cancellation.

cancelAgent doesn't dispatch any action on success, so the UI won't reflect the cancellation until the next WebSocket event or manual refresh. Consider optimistically updating the agent status or refreshing the agent data after the API call succeeds.

♻️ Proposed enhancement to update state on success
 export function cancelAgent(agentId: string) {
-    return async () => {
+    return async (dispatch: (action: PluginAction) => void) => {
         try {
             await Client.cancelAgent(agentId);
+            // Refresh agent to get updated status
+            const agent = await Client.getAgent(agentId);
+            dispatch({type: AGENT_RECEIVED, data: agent});
         } catch (error) {
             console.error('Failed to cancel agent:', error); // eslint-disable-line no-console
         }
     };
 }
webapp/src/index.tsx (2)

14-15: rhsToggleAction is stored but never used.

The rhsToggleAction field is assigned but never referenced elsewhere in the file. Only rhsShowAction is used (in the "View Agent Details" action). Consider removing the unused field if it's not planned for future use.

🧹 Proposed cleanup
 export default class Plugin {
-    private rhsToggleAction: object | null = null;
     private rhsShowAction: object | null = null;

And update the destructuring:

-        const {toggleRHSPlugin, showRHSPlugin} = registry.registerRightHandSidebarComponent(
+        const {showRHSPlugin} = registry.registerRightHandSidebarComponent(
             RHSPanel,
             'Cursor Agents',
         );
-        this.rhsToggleAction = toggleRHSPlugin;
         this.rhsShowAction = showRHSPlugin;

29-35: Note: toggleRHSPlugin is used in App Bar registration.

Looking more closely, toggleRHSPlugin is used in the App Bar callback (line 32), so while rhsToggleAction field is unused, the toggleRHSPlugin variable itself is needed. The field storage is indeed unnecessary.

Comment thread webapp/src/actions.ts
Comment thread webapp/src/components/rhs/AgentCard.tsx
- Fix registerPostDropdownMenuAction type signature to match runtime API
- Cast reducer and selectAgent dispatches for Redux 5 UnknownAction compat
- All tsc --noEmit, ESLint, tests, and build pass

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@webapp/src/types/mattermost-webapp/index.d.ts`:
- Around line 527-537: The declared signature for registerPostDropdownMenuAction
incorrectly types the filter as (postId: string) => boolean; change both
overloads so the filter parameter is (post: Post) => boolean to match the real
API and SDK docs, keeping action as (postId: string) => void; update the type
reference to use the existing Post type (import or fully-qualified name used
elsewhere in this d.ts) so consumers receive the actual Post object instead of a
postId.
🧹 Nitpick comments (3)
webapp/src/index.tsx (3)

14-15: Remove unused rhsToggleAction field.

rhsToggleAction is assigned at line 26 but never used. Only rhsShowAction is referenced (line 113). Consider removing the unused field to reduce confusion.

🧹 Suggested cleanup
 export default class Plugin {
-    private rhsToggleAction: object | null = null;
     private rhsShowAction: object | null = null;

And at line 26:

-        const {toggleRHSPlugin, showRHSPlugin} = registry.registerRightHandSidebarComponent(
+        const {showRHSPlugin} = registry.registerRightHandSidebarComponent(
             RHSPanel,
             'Cursor Agents',
         );
-        this.rhsToggleAction = toggleRHSPlugin;
         this.rhsShowAction = showRHSPlugin;

54-76: Consider user feedback when agent ID is missing.

If the user provides follow-up text but agentId is not found (lines 62-64), the action silently does nothing. Consider showing an error notification to the user in this edge case.


52-123: Consider extracting repeated state access into a helper.

The pattern state.entities?.posts?.posts?.[postId] appears six times throughout registerPostActions. Extracting this to a small helper function would reduce duplication and centralize the state shape dependency.

♻️ Optional helper extraction
// At module level or as a private method
const getPost = (state: any, postId: string) => state.entities?.posts?.posts?.[postId];

// Usage in actions:
const post = getPost(store.getState(), postId);

Comment thread webapp/src/types/mattermost-webapp/index.d.ts
nickmisasi and others added 5 commits February 13, 2026 11:23
- Check all error returns from Body.Close, Write, Encode, Decode
- Fix gofmt formatting in configuration.go, plugin.go, handlers_test.go

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…dernize)

- Check all remaining unchecked error returns (Encode, Close, Write)
- Fix gofmt formatting in api_test.go, plugin.go, poller.go
- Lowercase error strings per staticcheck ST1005
- Remove unnecessary fmt.Sprintf per staticcheck S1039
- Add nolint:gosec for test webhook secret constant
- Use strings.Cut and strings.SplitSeq per modernize linter

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Accepted fixes:
- CR-1: Replace literal API key in curl examples with env var pattern
- CR-2: Return HTTP 502 when cancelling agent with nil Cursor client
- CR-3+6: Consolidate duplicate sanitizeBranchName, add empty slug fallback
- CR-4: Populate missing fields in /cursor command agent save
- CR-7: Use CursorClientFn getter to avoid stale client reference
- CR-8: Only mark webhook delivery processed after handler succeeds
- CR-10: Fix deprecated word-break: break-word -> overflow-wrap: anywhere
- CR-11: Add parseTimestamp helper with NaN fallback for WebSocket events
- CR-12: Add Space key handling for WCAG keyboard accessibility

Rejected with PR replies:
- CR-5: POST retries are intentional (idempotent operations, no retry keys)
- CR-9: MERGED/PR_CLOSED statuses are intentional (extended lifecycle tracking)
- CR-10 (partial): rgba() with CSS vars is incompatible with modern slash syntax
- CR-13: filter param receives postId at runtime, not Post object

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant