open-swe/agent/dashboard/enabled_repos.py

69 lines
2 KiB
Python
Raw Permalink Normal View History

feat: restructure Open SWE Review tab + wire create_prs (#1319) * feat(dashboard): restructure Open SWE Review tab + wire create_prs Restructures the dashboard around two related changes the reviewer settings have been asking for: - Wire profile.create_prs. Defaults to true (opt-out); when off the system prompt gets a `Pull Request Policy Override` section telling the agent to push the branch and notify with the branch URL instead of opening a PR. Removes the noop Slack Notifications / Allow Artifacts / First Name / Last Name controls and their schema fields. - Repositories opt-in for Open SWE Review. New per-team enabled list stored in the LangGraph Store (`["enabled_review_repos"]`). Every reviewer webhook chokepoint now goes through `_is_repo_enabled_for_review` which AND-combines the existing env allowlist with the dashboard list. Default is empty (opt-in) — admins enable repos per-installation from the new Repositories page nested under Open SWE Review. - Open SWE Review tab now mirrors the Cursor "rules" pattern: main page shows installation rows + a Rules entry; both drill into nested pages (/review/repositories/$owner and /review/styles) with a back link. - Adds the new logo/favicon assets shipped from sidebar + html head. Tests pass with a new autouse fixture (`tests/conftest.py`) that defaults `is_review_repo_enabled` to True for existing allowlist tests. * fix(dashboard): make main content scroll independently of the sidebar Outer flex container was min-h-svh, so it grew with main's content and the whole page scrolled — sidebar moved with it. Pin to h-svh + overflow-hidden so the sidebar stays put and only <main> scrolls. * fix(dashboard): make disabled repo toggles obviously disabled Switch's disabled state used opacity-50 against a muted background, so the not-admin state looked nearly identical to the off state. Bump to opacity-40 + grayscale, and wrap each repo toggle in a span carrying a native hover tooltip explaining why it's disabled. * fix(switch): handle base-ui's data-disabled state base-ui's Switch.Root sets data-disabled (not the HTML disabled attribute) when disabled, so Tailwind's disabled: variant never matches and the button keeps its cursor-pointer + clickable look. Mirror the styling under the data-[disabled] variant and add pointer-events-none so the disabled state is both visible and actually unclickable. * feat(dashboard): paginate per-installation repository list 20 repos per page with Prev / page X of Y / Next controls at the bottom. Pager only renders when there are more than 20 repos. Page resets to 0 when navigating between installations. * feat(dashboard): global default model selectors for Agent + Reviewer Adds team-wide default model + reasoning effort for both agents in the Admin tab so operators can switch models without redeploying. Resolution chain: Agent: hardcoded -> LLM_MODEL_ID env -> team default -> user profile Reviewer: hardcoded -> LLM_MODEL_ID env -> team default -> per-call configurable Team defaults live in team_settings and are validated against the SUPPORTED_MODELS allowlist + the model's supported reasoning efforts. 'Inherit from env' clears the override and falls back to LLM_MODEL_ID. * refactor(models): drop LLM_MODEL_ID env in favour of the team default The team default is now the single source of truth for the runtime model choice; per-user (agent) and per-call configurable (reviewer) selections still win on top. When no admin has touched the team default, it surfaces the hardcoded fallback (DEFAULT_MODEL_ID + its default effort), so the admin UI's dropdown is always pre-populated with a sensible value. The Admin UI loses the 'Inherit from env' option since there is no longer an env layer to inherit from. * chore(models): set hardcoded fallback to gpt-5.5 medium Decouple the team-default boot value (gpt-5.5 / medium) from each model's ProfileForm-suggested default_effort so we can change one without nudging the other. The Opus xhigh default for new user profiles is unchanged. * feat(dashboard): trigger-mode copy, Coming Soon badges, logout in My Settings - Rename trigger mode 'ready_for_review' -> 'once_per_pr' with new description copy that matches the screenshot. Legacy stored values fall back to 'every_push' on read so the UI never shows an unknown selection. - Add a 'Coming soon' badge + greyed-out + disabled state on the controls that don't have runtime consumers yet: Trigger Mode, Autofix Mode, Autofix Severity Threshold, and Automatically fix CI failures. SettingsRow grew a comingSoon prop to keep this consistent. - My Settings drops the noop PR Preferences section and adds a Sign Out button. preferred_pr_destination is removed from the profile schema; old records get the field popped on next write. --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-05-21 09:17:07 -07:00
"""Team-wide opt-in list of repos that Open SWE Review may auto-review.
A single record keyed ``"default"`` holds the list. Repos default to
**disabled** — webhooks for repos absent from the list are ignored, so an
operator who installs the GitHub App into a new org doesn't get surprise
review comments on every PR.
"""
from __future__ import annotations
import logging
from datetime import UTC, datetime
from langgraph_sdk import get_client
from .review_styles import normalize_repo_full_name
logger = logging.getLogger(__name__)
ENABLED_REVIEW_REPOS_NAMESPACE: list[str] = ["enabled_review_repos"]
ENABLED_REVIEW_REPOS_KEY = "default"
def _client():
return get_client()
async def list_enabled_review_repos() -> list[str]:
try:
item = await _client().store.get_item(
ENABLED_REVIEW_REPOS_NAMESPACE, ENABLED_REVIEW_REPOS_KEY
)
except Exception as e:
logger.debug("enabled review repos lookup failed: %s", e)
return []
if item is None:
return []
value = item.get("value") if isinstance(item, dict) else getattr(item, "value", None)
if not isinstance(value, dict):
return []
repos = value.get("repos")
if not isinstance(repos, list):
return []
return [r for r in repos if isinstance(r, str)]
async def set_review_repo_enabled(full_name: str, enabled: bool) -> list[str]:
full_name = normalize_repo_full_name(full_name)
current = set(await list_enabled_review_repos())
if enabled:
current.add(full_name)
else:
current.discard(full_name)
repos = sorted(current)
await _client().store.put_item(
ENABLED_REVIEW_REPOS_NAMESPACE,
ENABLED_REVIEW_REPOS_KEY,
{"repos": repos, "updated_at": datetime.now(UTC).isoformat()},
)
return repos
async def is_review_repo_enabled(owner: str, name: str) -> bool:
if not owner or not name:
return False
full_name = f"{owner.lower()}/{name.lower()}"
enabled = await list_enabled_review_repos()
return any(r.lower() == full_name for r in enabled)