fix: separate review access from automatic reviews (upstream #1720) (#198)

* fix: separate review access from automatic reviews (#1720)

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
(cherry picked from commit 92d631704e9c38f6aeedf70baede85a7c638567e)

* fix: rename fork-only auto-fix gates to _is_repo_auto_review_enabled

The cherry-pick of upstream 92d63170 renamed _is_repo_enabled_for_review
to _is_repo_auto_review_enabled, but the fork's CI auto-fix, auto-fix
toggle command, and auto-fix review-feedback gates (not present upstream)
still referenced the old name and would raise NameError at request time.
Rename them in place — auto-fix surfaces stay gated on the dashboard
auto-review opt-in list, preserving current fork behavior.

---------

Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Adam Moussa 2026-07-17 12:20:36 -04:00 • committed by GitHub
parent 032d3889e4
commit c80bd0f92f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 128 deletions

View file

@ -646,12 +646,8 @@ def _is_repo_allowed(repo_config: dict[str, str]) -> bool:
return False
async def _is_repo_enabled_for_review(repo_config: dict[str, str]) -> bool:
"""Check the dashboard opt-in list for reviewer-agent entrypoints.
The opt-in list is empty by default, so repos are off until an admin
enables them in the dashboard's Open SWE Review tab.
"""
async def _is_repo_auto_review_enabled(repo_config: dict[str, str]) -> bool:
"""Return whether automatic reviews are enabled for a repository."""
return await is_review_repo_enabled(repo_config.get("owner", ""), repo_config.get("name", ""))
@ -2421,14 +2417,12 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
if action in _GH_PR_AGENT_STATE_ACTIONS:
background_tasks.add_task(update_agent_thread_pr_state, payload)
if action in _GH_PR_WATCH_TOGGLE_ACTIONS:
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
logger.info("Accepted GitHub PR %s webhook, scheduling reviewer watch update", action)
background_tasks.add_task(process_github_pr_close, payload)
return {"status": "accepted", "message": f"Processing PR {action} for reviewer watch"}
if action in _GH_PR_FIRST_REVIEW_ACTIONS:
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
if not await _is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
gate_rejection = await _enforce_public_repo_org_gate(payload, "pull_request")
if gate_rejection is not None:
return gate_rejection
@ -2442,8 +2436,8 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
}
if event_type == "push":
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
if not await _is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
logger.info("Accepted GitHub push webhook, scheduling reviewer watch evaluation")
background_tasks.add_task(process_github_push_event, payload)
return {"status": "accepted", "message": "Processing GitHub push for reviewer watch"}
@ -2451,8 +2445,8 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
if event_type in _GH_CI_EVENTS:
if not is_failing_ci_payload(payload, event_type):
return {"status": "ignored", "reason": "CI event is not a completed failure"}
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
if not await _is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
logger.info("Accepted GitHub %s webhook, scheduling CI auto-fix evaluation", event_type)
background_tasks.add_task(process_github_ci_event, payload, event_type)
return {"status": "accepted", "message": f"Processing GitHub {event_type} for auto-fix"}
@ -2507,8 +2501,8 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
}
autofix_command = _parse_autofix_command(comment_body)
if autofix_command is not None and is_pr_related_comment:
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
if not await _is_repo_auto_review_enabled(webhook_repo_config):
return {"status": "ignored", "reason": "Automatic review disabled for repository"}
gate_rejection = await _enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
@ -2521,8 +2515,6 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
event_type == "pull_request_review_comment"
and _review_comment_reply_parent_id(payload) is not None
):
if not await _is_repo_enabled_for_review(webhook_repo_config):
return {"status": "ignored", "reason": "Repository not enabled for review"}
gate_rejection = await _enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection
@ -2530,9 +2522,9 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
return {"status": "accepted", "message": "Processing review finding reply"}
if not any(tag in comment_body.lower() for tag in OPEN_SWE_TAGS):
if _is_actionable_review_payload(payload, event_type) and await _is_repo_enabled_for_review(
webhook_repo_config
):
if _is_actionable_review_payload(
payload, event_type
) and await _is_repo_auto_review_enabled(webhook_repo_config):
gate_rejection = await _enforce_public_repo_org_gate(payload, event_type)
if gate_rejection is not None:
return gate_rejection

View file

@ -107,8 +107,6 @@ async def trigger_pr_review_from_ref(
slack_thread_ts: str = "",
) -> dict[str, Any]:
repo_config = {"owner": pr_ref.owner, "name": pr_ref.repo}
if not await webapp._is_repo_enabled_for_review(repo_config):
return {"success": False, "error": "Repository not enabled for review"}
# Full token to read PR metadata (privacy/id aren't in the trigger ref);
# re-scoped below once we know whether the repo is public.
@ -368,8 +366,6 @@ async def process_github_pr_close(payload: dict[str, Any]) -> None:
pr_number = pull_request.get("number")
if not pr_number or not isinstance(pr_number, int):
return
if not await webapp._is_repo_enabled_for_review(repo_config):
return
thread_id = webapp.generate_reviewer_thread_id(
repo_config.get("owner", ""), repo_config.get("name", ""), pr_number
@ -432,9 +428,9 @@ async def process_github_push_event(payload: dict[str, Any]) -> None:
"Push to %s ignored: repository owner/name missing from payload", head_ref
)
return
if not await webapp._is_repo_enabled_for_review(repo_config):
if not await webapp._is_repo_auto_review_enabled(repo_config):
webapp.logger.info(
"Push to %s/%s head=%s ignored: repo not enabled for review",
"Push to %s/%s head=%s ignored: automatic review disabled",
repo_config["owner"],
repo_config["name"],
head_ref,

View file

@ -8,13 +8,13 @@ from agent import webapp
@pytest.fixture(autouse=True)
def _default_enable_review_repos(monkeypatch: pytest.MonkeyPatch) -> None:
"""Treat every repo as enabled for review by default.
def _default_enable_auto_review(monkeypatch: pytest.MonkeyPatch) -> None:
"""Treat automatic reviews as enabled for every repo by default.
The dashboard's opt-in list (loaded by :func:`agent.dashboard.enabled_repos.is_review_repo_enabled`)
is empty in the test environment because there is no live LangGraph Store.
Tests targeting the opt-in gate itself should override this fixture or set
Tests targeting the automatic-review gate should override this fixture or set
``monkeypatch.setattr(webapp, "is_review_repo_enabled", ...)`` to a stricter stub.
"""

View file

@ -105,7 +105,7 @@ def test_build_github_issue_followup_prompt_only_includes_comment() -> None:
assert "## Title" not in prompt
def test_reviewer_enablement_uses_dashboard_opt_in(monkeypatch) -> None:
def test_auto_review_enablement_uses_dashboard_opt_in(monkeypatch) -> None:
seen: dict[str, str] = {}
async def fake_is_review_repo_enabled(owner: str, name: str) -> bool:
@ -117,19 +117,52 @@ def test_reviewer_enablement_uses_dashboard_opt_in(monkeypatch) -> None:
assert (
asyncio.run(
webapp._is_repo_enabled_for_review({"owner": "langchain-ai", "name": "open-swe-app"})
webapp._is_repo_auto_review_enabled({"owner": "langchain-ai", "name": "open-swe-app"})
)
is True
)
assert seen == {"owner": "langchain-ai", "name": "open-swe-app"}
assert (
asyncio.run(
webapp._is_repo_enabled_for_review({"owner": "langchain-ai", "name": "open-swe"})
webapp._is_repo_auto_review_enabled({"owner": "langchain-ai", "name": "open-swe"})
)
is False
)
def test_github_webhook_skips_automatic_review_when_disabled(monkeypatch) -> None:
called = False
async def fake_auto_review_enabled(_repo_config: dict[str, str]) -> bool:
return False
async def fake_process_github_pr_ready(_payload: dict[str, object]) -> None:
nonlocal called
called = True
monkeypatch.setattr(webapp, "_is_repo_auto_review_enabled", fake_auto_review_enabled)
monkeypatch.setattr(webapp, "process_github_pr_ready", fake_process_github_pr_ready)
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
client = TestClient(webapp.app)
response = _post_github_webhook(
client,
"pull_request",
{
"action": "opened",
"repository": {"owner": {"login": "langchain-ai"}, "name": "open-swe"},
"pull_request": {"number": 1244},
},
)
assert response.status_code == 200
assert response.json() == {
"status": "ignored",
"reason": "Automatic review disabled for repository",
}
assert called is False
def test_github_webhook_accepts_issue_events(monkeypatch) -> None:
called: dict[str, object] = {}
@ -259,17 +292,20 @@ def test_github_webhook_ignores_unmentioned_comment_without_info_log(monkeypatch
def test_github_webhook_routes_review_comment_reply_without_tag(monkeypatch) -> None:
called: dict[str, object] = {}
auto_review_checked = False
async def fake_process_github_review_finding_reply(payload: dict[str, object]) -> None:
called["payload"] = payload
async def fake_repo_enabled(_repo_config: dict[str, str]) -> bool:
return True
async def fake_auto_review_enabled(_repo_config: dict[str, str]) -> bool:
nonlocal auto_review_checked
auto_review_checked = True
return False
monkeypatch.setattr(
webapp, "process_github_review_finding_reply", fake_process_github_review_finding_reply
)
monkeypatch.setattr(webapp, "_is_repo_enabled_for_review", fake_repo_enabled)
monkeypatch.setattr(webapp, "_is_repo_auto_review_enabled", fake_auto_review_enabled)
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
client = TestClient(webapp.app)
@ -295,6 +331,7 @@ def test_github_webhook_routes_review_comment_reply_without_tag(monkeypatch) ->
assert response.status_code == 200
assert response.json()["status"] == "accepted"
assert auto_review_checked is False
payload = called["payload"]
assert isinstance(payload, dict)
assert payload["comment"]["in_reply_to_id"] == 111
@ -917,6 +954,12 @@ def test_process_github_pr_ready_creates_reviewer_run(monkeypatch) -> None:
def test_trigger_pr_review_from_ref_creates_reviewer_run(monkeypatch) -> None:
captured: dict[str, object] = {}
auto_review_checked = False
async def fake_auto_review_enabled(_repo_config: dict[str, str]) -> bool:
nonlocal auto_review_checked
auto_review_checked = True
return False
async def fake_get_github_app_installation_token() -> str | None:
return "app-token"
@ -959,6 +1002,7 @@ def test_trigger_pr_review_from_ref_creates_reviewer_run(monkeypatch) -> None:
captured["set_metadata_thread_id"] = thread_id
captured["set_metadata_kwargs"] = kwargs
monkeypatch.setattr(webapp, "_is_repo_auto_review_enabled", fake_auto_review_enabled)
monkeypatch.setattr(
webapp, "get_github_app_installation_token", fake_get_github_app_installation_token
)
@ -996,6 +1040,7 @@ def test_trigger_pr_review_from_ref_creates_reviewer_run(monkeypatch) -> None:
prompt = kwargs["input"]["messages"][0]["content"]
config = kwargs["config"]["configurable"]
assert result["success"] is True
assert auto_review_checked is False
assert captured["graph"] == "reviewer"
assert captured["thread_create_kwargs"] == {
"thread_id": captured["thread_id"],
@ -1019,39 +1064,6 @@ def test_trigger_pr_review_from_ref_creates_reviewer_run(monkeypatch) -> None:
assert captured["status_comment_kwargs"]["pr_number"] == 1244
def test_trigger_pr_review_from_ref_respects_dashboard_opt_in(monkeypatch) -> None:
called = False
async def fake_get_github_app_installation_token() -> str | None:
nonlocal called
called = True
return "app-token"
monkeypatch.setattr(
webapp, "get_github_app_installation_token", fake_get_github_app_installation_token
)
async def fake_is_review_repo_enabled(_owner: str, _name: str) -> bool:
return False
monkeypatch.setattr(webapp, "is_review_repo_enabled", fake_is_review_repo_enabled)
result = asyncio.run(
webapp.trigger_pr_review_from_ref(
GitHubPrRef(
owner="langchain-ai",
repo="blocked",
number=1,
url="https://github.com/langchain-ai/blocked/pull/1",
),
source="slack",
)
)
assert result == {"success": False, "error": "Repository not enabled for review"}
assert called is False
async def test_request_pr_review_tool_uses_shared_trigger(monkeypatch) -> None:
captured: dict[str, object] = {}

View file

@ -46,7 +46,7 @@ async def test_push_event_skips_branch_deletion() -> None:
ref="refs/heads/feat-x", after="0000000000000000000000000000000000000000"
)
with patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
):
await webapp.process_github_push_event(payload)
# If we got here without crashing and with no other patches needed, the
@ -68,7 +68,7 @@ async def test_push_event_skips_when_thread_not_watching() -> None:
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp.get_github_app_installation_token",
@ -107,7 +107,7 @@ async def test_push_event_skips_when_pr_diff_unchanged_since_last_review() -> No
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp.get_github_app_installation_token_with_expiry",
@ -175,7 +175,7 @@ async def test_push_event_triggers_re_review_run_when_watching() -> None:
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp.get_github_app_installation_token",
@ -267,7 +267,7 @@ async def test_push_event_idempotent_when_head_unchanged() -> None:
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp.get_github_app_installation_token",
@ -352,7 +352,7 @@ async def test_push_event_public_repo_uses_scoped_token() -> None:
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch("agent.webapp.get_github_app_installation_token_with_expiry", get_token),
patch("agent.webapp._fetch_open_pr_for_branch", new_callable=AsyncMock, return_value=pr),
@ -396,7 +396,7 @@ async def test_push_event_rescopes_token_when_pr_metadata_reveals_public() -> No
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=True
),
patch("agent.webapp.get_github_app_installation_token_with_expiry", get_token),
patch("agent.webapp._fetch_open_pr_for_branch", new_callable=AsyncMock, return_value=pr),
@ -432,8 +432,8 @@ async def test_pr_close_disables_watch() -> None:
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
),
"agent.webapp._is_repo_auto_review_enabled", new_callable=AsyncMock, return_value=False
) as auto_review_enabled,
patch(
"agent.webapp._get_thread_metadata_safe",
new_callable=AsyncMock,
@ -442,6 +442,7 @@ async def test_pr_close_disables_watch() -> None:
patch("agent.webapp.set_reviewer_thread_metadata", side_effect=fake_set),
):
await webapp.process_github_pr_close(_pr_close_payload(action="closed"))
auto_review_enabled.assert_not_awaited()
assert captured and captured[0][1]["watch"] is False
@ -453,9 +454,6 @@ async def test_pr_reopened_re_enables_watch() -> None:
captured.append((thread_id, kwargs))
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp._get_thread_metadata_safe",
new_callable=AsyncMock,
@ -471,9 +469,6 @@ async def test_pr_reopened_re_enables_watch() -> None:
async def test_pr_close_skips_non_reviewer_threads() -> None:
fake_set = AsyncMock()
with (
patch(
"agent.webapp._is_repo_enabled_for_review", new_callable=AsyncMock, return_value=True
),
patch(
"agent.webapp._get_thread_metadata_safe",
new_callable=AsyncMock,

View file

@ -733,12 +733,12 @@ export const api = {
request<NotionCredentialStatus>("/my-credentials/notion", {
method: "DELETE",
}),
listEnabledReviewRepos: () =>
listAutoReviewRepos: () =>
request<{ repos: Array<string> }>("/enabled-review-repos"),
setEnabledReviewRepo: (full_name: string, enabled: boolean) =>
setAutoReviewRepo: (full_name: string, runAutomatically: boolean) =>
request<{ repos: Array<string> }>("/enabled-review-repos", {
method: "PUT",
body: JSON.stringify({ full_name, enabled }),
body: JSON.stringify({ full_name, enabled: runAutomatically }),
}),
usageLeaderboard: (period: UsageLeaderboardPeriod = "30d", limit = 10) =>
request<UsageLeaderboardPayload>(

View file

@ -91,7 +91,7 @@ function ReviewPage() {
<AppShell
user={session.data}
title="Open SWE Review"
description="Automatically review pull requests for bugs and issues. Runs are billed based on underlying agent usage."
description="Review pull requests for bugs and issues on demand, or run reviews automatically. Runs are billed based on underlying agent usage."
>
<RepositoriesSection canEdit={canEdit} />
@ -202,14 +202,14 @@ function RepositoriesSection({ canEdit: _canEdit }: { canEdit: boolean }) {
},
});
const enabled = useQuery({
queryKey: ["enabledReviewRepos"],
queryFn: api.listEnabledReviewRepos,
const autoReview = useQuery({
queryKey: ["autoReviewRepos"],
queryFn: api.listAutoReviewRepos,
});
const enabledSet = useMemo(
() => new Set(enabled.data?.repos ?? []),
[enabled.data?.repos],
const autoReviewSet = useMemo(
() => new Set(autoReview.data?.repos ?? []),
[autoReview.data?.repos],
);
const grouped = useMemo(() => {
@ -224,12 +224,12 @@ function RepositoriesSection({ canEdit: _canEdit }: { canEdit: boolean }) {
return Array.from(byOwner.entries()).sort(([a], [b]) => a.localeCompare(b));
}, [repos.data?.repositories]);
const loading = repos.isLoading || enabled.isLoading;
const loading = repos.isLoading || autoReview.isLoading;
return (
<SettingsSection
title="Repositories"
description="Source-control installations. Click into one to enable repos for automatic review."
description="All installed repositories support on-demand reviews. Click into an installation to configure automatic reviews."
>
<div className="divide-y divide-border">
{loading && (
@ -244,7 +244,7 @@ function RepositoriesSection({ canEdit: _canEdit }: { canEdit: boolean }) {
</p>
)}
{grouped.map(([owner, list]) => {
const enabledCount = list.filter((r) => enabledSet.has(r.full_name)).length;
const autoReviewCount = list.filter((r) => autoReviewSet.has(r.full_name)).length;
return (
<Link
key={owner}
@ -263,7 +263,7 @@ function RepositoriesSection({ canEdit: _canEdit }: { canEdit: boolean }) {
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<span>
{enabledCount}/{list.length} Repositories Enabled
{autoReviewCount}/{list.length} Run Automatically
</span>
<CaretRightIcon className="size-3.5" />
</div>

View file

@ -36,17 +36,17 @@ function RepositoriesOwnerPage() {
enabled: !!session.data,
});
const enabled = useQuery({
queryKey: ["enabledReviewRepos"],
queryFn: api.listEnabledReviewRepos,
const autoReview = useQuery({
queryKey: ["autoReviewRepos"],
queryFn: api.listAutoReviewRepos,
enabled: !!session.data,
});
const toggle = useMutation({
const toggleAutoReview = useMutation({
mutationFn: ({ full_name, on }: { full_name: string; on: boolean }) =>
api.setEnabledReviewRepo(full_name, on),
api.setAutoReviewRepo(full_name, on),
onSuccess: (data) => {
qc.setQueryData(["enabledReviewRepos"], data);
qc.setQueryData(["autoReviewRepos"], data);
},
});
@ -58,9 +58,9 @@ function RepositoriesOwnerPage() {
[repos.data?.repositories, owner],
);
const enabledSet = useMemo(
() => new Set(enabled.data?.repos ?? []),
[enabled.data?.repos],
const autoReviewSet = useMemo(
() => new Set(autoReview.data?.repos ?? []),
[autoReview.data?.repos],
);
const [page, setPage] = useState(0);
@ -82,8 +82,8 @@ function RepositoriesOwnerPage() {
if (!session.data) return <RequireLogin />;
const canEdit = session.data.is_admin;
const enabledCount = ownerRepos.filter((r) => enabledSet.has(r.full_name)).length;
const loading = repos.isLoading || enabled.isLoading;
const autoReviewCount = ownerRepos.filter((r) => autoReviewSet.has(r.full_name)).length;
const loading = repos.isLoading || autoReview.isLoading;
return (
<AppShell
@ -91,8 +91,8 @@ function RepositoriesOwnerPage() {
title={owner}
description={
canEdit
? "Toggle a repository to opt it into automatic Open SWE Review."
: "Only team admins can modify enabled repositories."
? "Choose which repositories run Open SWE Review automatically. All installed repositories remain available for on-demand reviews."
: "Automatic review settings are read-only for non-admins."
}
backTo={{ to: "/review", label: "Back to Open SWE Review" }}
>
@ -102,7 +102,7 @@ function RepositoriesOwnerPage() {
Repositories
</h2>
<span className="text-xs text-muted-foreground">
{enabledCount}/{ownerRepos.length} enabled
{autoReviewCount}/{ownerRepos.length} run automatically
</span>
</div>
<div className="rounded-lg border border-border bg-card">
@ -118,7 +118,7 @@ function RepositoriesOwnerPage() {
)}
<ul className="divide-y divide-border">
{pageRepos.map((r) => {
const isEnabled = enabledSet.has(r.full_name);
const runsAutomatically = autoReviewSet.has(r.full_name);
return (
<li
key={r.full_name}
@ -135,22 +135,26 @@ function RepositoriesOwnerPage() {
<span className="text-[10px] text-muted-foreground">private</span>
)}
</div>
<span
title={
!canEdit
? "Only team admins can modify enabled repositories"
: undefined
}
className={!canEdit ? "cursor-not-allowed" : undefined}
>
<Switch
checked={isEnabled}
disabled={!canEdit || toggle.isPending}
onCheckedChange={(v) =>
toggle.mutate({ full_name: r.full_name, on: v })
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground">Run automatically</span>
<span
title={
!canEdit
? "Only team admins can modify automatic review settings"
: undefined
}
/>
</span>
className={!canEdit ? "cursor-not-allowed" : undefined}
>
<Switch
aria-label={`Run reviews automatically for ${r.full_name}`}
checked={runsAutomatically}
disabled={!canEdit || toggleAutoReview.isPending}
onCheckedChange={(v) =>
toggleAutoReview.mutate({ full_name: r.full_name, on: v })
}
/>
</span>
</div>
</li>
);
})}