From f327c528da3ec8075905aa67d137c219c4e038f2 Mon Sep 17 00:00:00 2001 From: Gavin Williams Date: Tue, 5 May 2026 17:15:33 +0100 Subject: [PATCH 1/4] feat(web): acknowledge review command comments with a reaction Adds a configurable acknowledgment reaction/emoji to the triggering comment when the review agent receives a `/review` command, so users get immediate feedback that the request was received. - GitHub: reacts to the issue comment via the reactions API (default: `eyes`) - GitLab: awards an emoji on the MR note via `MergeRequestNoteAwardEmojis` - New `REVIEW_AGENT_ACK_REACTION` env var (default: `eyes`) controls the reaction - Acknowledgment failures are logged as warnings and do not block the review Co-Authored-By: Claude Sonnet 4.6 --- .../configuration/environment-variables.mdx | 3 ++- docs/docs/features/agents/review-agent.mdx | 5 ++-- packages/shared/src/env.server.ts | 5 ++-- .../web/src/app/api/(server)/webhook/route.ts | 23 +++++++++++++++++++ .../src/features/agents/review-agent/types.ts | 1 + 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/docs/configuration/environment-variables.mdx b/docs/docs/configuration/environment-variables.mdx index 875a32dbe..53890ed70 100644 --- a/docs/docs/configuration/environment-variables.mdx +++ b/docs/docs/configuration/environment-variables.mdx @@ -70,6 +70,7 @@ The following environment variables allow you to configure your Sourcebot deploy | `GITHUB_REVIEW_AGENT_APP_PRIVATE_KEY_PATH` | `-` |

The container relative path to the private key file for the GitHub App used by the review agent.

| | `GITHUB_REVIEW_AGENT_APP_WEBHOOK_SECRET` | `-` |

The webhook secret for the GitHub App used by the review agent.

| | `OPENAI_API_KEY` | `-` |

The OpenAI API key used by the review agent.

| +| `REVIEW_AGENT_ACK_REACTION` | `eyes` |

The reaction/emoji added to a comment when the review agent acknowledges receipt of the review command.

| | `REVIEW_AGENT_API_KEY` | `-` |

The Sourcebot API key used by the review agent.

| | `REVIEW_AGENT_AUTO_REVIEW_ENABLED` | `false` |

Enables/disables automatic code reviews by the review agent.

| | `REVIEW_AGENT_LOGGING_ENABLED` | `true` |

Enables/disables logging for the review agent. Logs are saved in `DATA_CACHE_DIR/review-agent`

| @@ -77,4 +78,4 @@ The following environment variables allow you to configure your Sourcebot deploy ### Overriding environment variables from the config -You can override environment variables from the config file by using the `environmentOverrides` property. See [this doc](/docs/configuration/config-file#overriding-environment-variables-from-the-config) for more info. \ No newline at end of file +You can override environment variables from the config file by using the `environmentOverrides` property. See [this doc](/docs/configuration/config-file#overriding-environment-variables-from-the-config) for more info. diff --git a/docs/docs/features/agents/review-agent.mdx b/docs/docs/features/agents/review-agent.mdx index b4dc9c2f6..12971c2d6 100644 --- a/docs/docs/features/agents/review-agent.mdx +++ b/docs/docs/features/agents/review-agent.mdx @@ -141,7 +141,8 @@ You can also trigger a review manually by commenting `/review` on any PR or MR. | Variable | Default | Description | |---|---|---| +| `REVIEW_AGENT_ACK_REACTION` | `eyes` | Reaction/emoji added to acknowledge receipt of the review command | | `REVIEW_AGENT_AUTO_REVIEW_ENABLED` | `false` | Automatically review new and updated PRs/MRs | -| `REVIEW_AGENT_REVIEW_COMMAND` | `review` | Comment command that triggers a manual review (without the `/`) | -| `REVIEW_AGENT_MODEL` | first configured model | `displayName` of the language model to use for reviews | | `REVIEW_AGENT_LOGGING_ENABLED` | unset | Write prompt and response logs to disk for debugging | +| `REVIEW_AGENT_MODEL` | first configured model | `displayName` of the language model to use for reviews | +| `REVIEW_AGENT_REVIEW_COMMAND` | `review` | Comment command that triggers a manual review (without the `/`) | diff --git a/packages/shared/src/env.server.ts b/packages/shared/src/env.server.ts index 460afc29b..6e205f77e 100644 --- a/packages/shared/src/env.server.ts +++ b/packages/shared/src/env.server.ts @@ -235,10 +235,11 @@ const options = { GITLAB_REVIEW_AGENT_TOKEN: z.string().optional(), GITLAB_REVIEW_AGENT_HOST: z.string().default('gitlab.com').transform(s => s.replace(/^https?:\/\//, '').replace(/\/+$/, '')).refine(s => /^[a-z0-9.-]+$/i.test(s), { message: 'invalid hostname' }), // Review agent config - REVIEW_AGENT_MODEL: z.string().optional(), + REVIEW_AGENT_ACK_REACTION: z.string().default('eyes'), REVIEW_AGENT_API_KEY: z.string().optional(), - REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'), REVIEW_AGENT_AUTO_REVIEW_ENABLED: booleanSchema.default('false'), + REVIEW_AGENT_LOGGING_ENABLED: booleanSchema.default('true'), + REVIEW_AGENT_MODEL: z.string().optional(), REVIEW_AGENT_REVIEW_COMMAND: z.string().default('review'), ANTHROPIC_API_KEY: z.string().optional(), diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts index e7e380b49..5361be614 100644 --- a/packages/web/src/app/api/(server)/webhook/route.ts +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -185,6 +185,18 @@ export const POST = async (request: NextRequest) => { const owner = body.repository.owner.login; const octokit = await githubApp.getInstallationOctokit(body.installation.id); + + try { + await octokit.rest.reactions.createForIssueComment({ + owner, + repo: repositoryName, + comment_id: body.comment.id, + content: env.REVIEW_AGENT_ACK_REACTION as Parameters[0]['content'], + }); + } catch (error) { + logger.warn(`Failed to add acknowledgment reaction to GitHub comment: ${error}`); + } + const { data: pullRequest } = await octokit.rest.pulls.get({ owner, repo: repositoryName, @@ -246,6 +258,17 @@ export const POST = async (request: NextRequest) => { if (noteBody === `/${env.REVIEW_AGENT_REVIEW_COMMAND}`) { logger.info('Review agent review command received on GitLab MR, processing'); + try { + await gitlabClient.MergeRequestNoteAwardEmojis.award( + parsed.data.project.id, + parsed.data.merge_request.iid, + parsed.data.object_attributes.id, + env.REVIEW_AGENT_ACK_REACTION, + ); + } catch (error) { + logger.warn(`Failed to add acknowledgment emoji to GitLab note: ${error}`); + } + const mrPayload: GitLabMergeRequestPayload = { object_kind: "merge_request", object_attributes: { diff --git a/packages/web/src/features/agents/review-agent/types.ts b/packages/web/src/features/agents/review-agent/types.ts index 1a9aba1eb..cb4ce5771 100644 --- a/packages/web/src/features/agents/review-agent/types.ts +++ b/packages/web/src/features/agents/review-agent/types.ts @@ -87,6 +87,7 @@ export type GitLabMergeRequestPayload = z.infer Date: Tue, 5 May 2026 17:26:03 +0100 Subject: [PATCH 2/4] fix(web): fix type cast for GitHub reaction content Co-Authored-By: Claude Sonnet 4.6 --- packages/web/src/app/api/(server)/webhook/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts index 5361be614..86ae77247 100644 --- a/packages/web/src/app/api/(server)/webhook/route.ts +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -191,7 +191,7 @@ export const POST = async (request: NextRequest) => { owner, repo: repositoryName, comment_id: body.comment.id, - content: env.REVIEW_AGENT_ACK_REACTION as Parameters[0]['content'], + content: env.REVIEW_AGENT_ACK_REACTION as "-1" | "+1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes", }); } catch (error) { logger.warn(`Failed to add acknowledgment reaction to GitHub comment: ${error}`); From c22fe46fa3c8dc33316633d367e73e2aeefc9c4d Mon Sep 17 00:00:00 2001 From: Gavin Williams Date: Tue, 5 May 2026 17:27:28 +0100 Subject: [PATCH 3/4] chore: add changelog entry for review agent ack reaction Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af857c42c..80b6b06f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [Experimental] Review agent now acknowledges receipt of the `review` command by adding a reaction to the triggering comment. The reaction is configurable via `REVIEW_AGENT_ACK_REACTION` (default: `eyes`). [#1174](https://github.com/sourcebot-dev/sourcebot/pull/1174) + ## [4.17.1] - 2026-05-04 ### Added From 6ee674d697e21782b4143983bcb46372a9b47e64 Mon Sep 17 00:00:00 2001 From: Gavin Williams Date: Tue, 5 May 2026 17:28:32 +0100 Subject: [PATCH 4/4] fix(web): wrap ack reactions in 1500ms timeout to avoid blocking webhook Co-Authored-By: Claude Sonnet 4.6 --- .../web/src/app/api/(server)/webhook/route.ts | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/packages/web/src/app/api/(server)/webhook/route.ts b/packages/web/src/app/api/(server)/webhook/route.ts index 86ae77247..b006efe77 100644 --- a/packages/web/src/app/api/(server)/webhook/route.ts +++ b/packages/web/src/app/api/(server)/webhook/route.ts @@ -130,6 +130,16 @@ if (env.GITLAB_REVIEW_AGENT_TOKEN) { } } +const ACK_TIMEOUT_MS = 1500; + +const ackWithTimeout = (promise: Promise, label: string): Promise => { + const timeout = new Promise(resolve => setTimeout(resolve, ACK_TIMEOUT_MS)); + return Promise.race([promise, timeout]).then( + () => { /* success or timeout — proceed */ }, + (error) => { logger.warn(`${label}: ${error}`); }, + ); +}; + export const POST = async (request: NextRequest) => { const body = await request.json(); const headers = Object.fromEntries(Array.from(request.headers.entries(), ([key, value]) => [key.toLowerCase(), value])); @@ -186,16 +196,15 @@ export const POST = async (request: NextRequest) => { const octokit = await githubApp.getInstallationOctokit(body.installation.id); - try { - await octokit.rest.reactions.createForIssueComment({ + await ackWithTimeout( + octokit.rest.reactions.createForIssueComment({ owner, repo: repositoryName, comment_id: body.comment.id, content: env.REVIEW_AGENT_ACK_REACTION as "-1" | "+1" | "laugh" | "confused" | "heart" | "hooray" | "rocket" | "eyes", - }); - } catch (error) { - logger.warn(`Failed to add acknowledgment reaction to GitHub comment: ${error}`); - } + }), + 'Failed to add acknowledgment reaction to GitHub comment', + ); const { data: pullRequest } = await octokit.rest.pulls.get({ owner, @@ -258,16 +267,15 @@ export const POST = async (request: NextRequest) => { if (noteBody === `/${env.REVIEW_AGENT_REVIEW_COMMAND}`) { logger.info('Review agent review command received on GitLab MR, processing'); - try { - await gitlabClient.MergeRequestNoteAwardEmojis.award( + await ackWithTimeout( + gitlabClient.MergeRequestNoteAwardEmojis.award( parsed.data.project.id, parsed.data.merge_request.iid, parsed.data.object_attributes.id, env.REVIEW_AGENT_ACK_REACTION, - ); - } catch (error) { - logger.warn(`Failed to add acknowledgment emoji to GitLab note: ${error}`); - } + ), + 'Failed to add acknowledgment emoji to GitLab note', + ); const mrPayload: GitLabMergeRequestPayload = { object_kind: "merge_request",