From 449cb5d1a8ad854d3ec3902ceec567e525d9984d Mon Sep 17 00:00:00 2001 From: Johannes du Plessis Date: Fri, 5 Jun 2026 13:48:47 -0700 Subject: [PATCH] fix: make default repository configurable (#1429) * fix: make default repository configurable Co-authored-by: open-swe[bot] * fix: preserve dashboard repo-less runs * fix: distinguish explicit repo-less dashboard runs --------- Co-authored-by: open-swe[bot] --- agent/dashboard/team_settings.py | 24 +++++ agent/dashboard/thread_api.py | 17 ++-- agent/prompt.py | 7 ++ agent/server.py | 23 ++++- agent/webapp.py | 18 +++- default_prompt.md | 2 +- tests/test_dashboard_repo_optional.py | 119 +++++++++++++++++++++++- tests/test_dashboard_web_handoff.py | 55 +++++++++++ tests/test_prompt_default_repo.py | 48 ++++++++++ tests/test_slack_context.py | 18 ++++ ui/src/components/agents/AgentsHome.tsx | 1 + ui/src/lib/agents/api.ts | 1 + ui/src/lib/api.ts | 1 + ui/src/routes/admin.tsx | 26 ++++++ 14 files changed, 347 insertions(+), 13 deletions(-) create mode 100644 tests/test_prompt_default_repo.py diff --git a/agent/dashboard/team_settings.py b/agent/dashboard/team_settings.py index 15df3e59..bf130f1a 100644 --- a/agent/dashboard/team_settings.py +++ b/agent/dashboard/team_settings.py @@ -8,6 +8,7 @@ configuration in one place. Per-repo style prompts live in from __future__ import annotations import logging +import os from datetime import UTC, datetime from typing import Any, Literal @@ -41,6 +42,7 @@ class TeamSettingsUpdate(BaseModel): default_agent_reasoning_effort: str | None = None default_agent_subagent_model: str | None = None default_agent_subagent_reasoning_effort: str | None = None + default_repo: str | None = None default_reviewer_model: str | None = None default_reviewer_reasoning_effort: str | None = None default_reviewer_subagent_model: str | None = None @@ -82,6 +84,21 @@ def _client(): return get_client() +def _env_default_repo() -> str | None: + owner = os.environ.get("DEFAULT_REPO_OWNER", "").strip() + name = os.environ.get("DEFAULT_REPO_NAME", "").strip() + return f"{owner}/{name}" if owner and name else None + + +def _parse_repo(value: object) -> dict[str, str] | None: + if not isinstance(value, str): + return None + owner, sep, name = value.strip().partition("/") + if not sep or not owner.strip() or not name.strip(): + return None + return {"owner": owner.strip(), "name": name.strip()} + + def _default_settings() -> dict[str, Any]: fallback_model, fallback_effort = default_model_pair() return { @@ -95,6 +112,7 @@ def _default_settings() -> dict[str, Any]: "default_agent_reasoning_effort": fallback_effort, "default_agent_subagent_model": fallback_model, "default_agent_subagent_reasoning_effort": fallback_effort, + "default_repo": _env_default_repo(), "default_reviewer_model": fallback_model, "default_reviewer_reasoning_effort": fallback_effort, "default_reviewer_subagent_model": fallback_model, @@ -138,6 +156,7 @@ async def upsert_team_settings(update: TeamSettingsUpdate) -> dict[str, Any]: "default_agent_reasoning_effort": update.default_agent_reasoning_effort, "default_agent_subagent_model": update.default_agent_subagent_model, "default_agent_subagent_reasoning_effort": update.default_agent_subagent_reasoning_effort, + "default_repo": update.default_repo, "default_reviewer_model": update.default_reviewer_model, "default_reviewer_reasoning_effort": update.default_reviewer_reasoning_effort, "default_reviewer_subagent_model": update.default_reviewer_subagent_model, @@ -148,6 +167,11 @@ async def upsert_team_settings(update: TeamSettingsUpdate) -> dict[str, Any]: return value +async def get_team_default_repo() -> dict[str, str] | None: + settings = await get_team_settings() + return _parse_repo(settings.get("default_repo")) + + async def get_team_default_model( role: Literal["agent", "reviewer"], ) -> tuple[str, str]: diff --git a/agent/dashboard/thread_api.py b/agent/dashboard/thread_api.py index 7971e264..bb076a0b 100644 --- a/agent/dashboard/thread_api.py +++ b/agent/dashboard/thread_api.py @@ -48,6 +48,7 @@ async def _resolve_run_email(login: str, profile: dict[str, Any]) -> str | None: class ThreadCreateBody(BaseModel): prompt: str = Field(min_length=1, max_length=20_000) repo: str | None = None + repo_explicitly_none: bool = False model_id: str | None = None effort: str | None = None @@ -269,13 +270,7 @@ async def get_dashboard_thread( def _resolve_repo_config(repo: str | None) -> dict[str, str]: - """Resolve the run's repo from the request, or ``{}`` when none is given. - - A repo is optional: the agent identifies and clones the target repo from the - task itself. The dashboard pre-fills the user's default repo on the client, - so the request value is authoritative here — an empty value means an - intentionally repo-less run, not "fall back to the saved default". - """ + """Resolve the run's repo from the request, or ``{}`` when none is given.""" return _parse_repo(repo) or {} @@ -284,6 +279,7 @@ async def _start_agent_run( *, login: str, repo_config: dict[str, str], + repo_explicitly_none: bool = False, prompt: str, title: str | None = None, model_id: str | None = None, @@ -309,6 +305,8 @@ async def _start_agent_run( if has_repo: metadata["repo_owner"] = repo_config["owner"] metadata["repo_name"] = repo_config["name"] + elif repo_explicitly_none: + metadata["repo_explicitly_none"] = True client = langgraph_client() await client.threads.create(thread_id=thread_id, metadata=metadata, if_exists="do_nothing") @@ -323,6 +321,8 @@ async def _start_agent_run( } if has_repo: configurable["repo"] = repo_config + elif repo_explicitly_none: + configurable["repo_explicitly_none"] = True if chosen_model and chosen_effort: configurable["agent_model_id"] = chosen_model configurable["agent_effort"] = chosen_effort @@ -354,6 +354,7 @@ async def create_dashboard_thread(login: str, body: ThreadCreateBody) -> dict[st thread_id, login=login, repo_config=repo_config, + repo_explicitly_none=body.repo_explicitly_none, prompt=body.prompt.strip(), model_id=body.model_id, effort=body.effort, @@ -404,6 +405,8 @@ async def send_dashboard_message( } if owner and name: configurable["repo"] = {"owner": owner, "name": name} + elif metadata.get("repo_explicitly_none") is True: + configurable["repo_explicitly_none"] = True if chosen_model and chosen_effort: configurable["agent_model_id"] = chosen_model configurable["agent_effort"] = chosen_effort diff --git a/agent/prompt.py b/agent/prompt.py index d662f1c0..278d1eef 100644 --- a/agent/prompt.py +++ b/agent/prompt.py @@ -429,8 +429,15 @@ def construct_system_prompt( linear_issue_number: str = "", triggering_user_identity: CollaboratorIdentity | None = None, create_prs: bool = False, + default_repo: dict[str, str] | None = None, ) -> str: default_prompt_section = _load_default_prompt() + if default_repo and default_repo.get("owner") and default_repo.get("name"): + repo_line = ( + "When a repository is not explicitly mentioned, use " + f"`{default_repo['owner']}/{default_repo['name']}`." + ) + default_prompt_section += f"\n\n{repo_line}" # Shell-escape: display names/emails are user-controlled (e.g. O'Connor) and # are embedded in a `git config` command the agent copies verbatim. if triggering_user_identity is not None: diff --git a/agent/server.py b/agent/server.py index bdadf262..1cd0a68c 100644 --- a/agent/server.py +++ b/agent/server.py @@ -39,7 +39,7 @@ from .dashboard.agent_overrides import ( ) from .dashboard.agent_usage import record_agent_thread_usage from .dashboard.options import DEFAULT_MODEL_ID, SUPPORTED_MODEL_IDS, model_supports_effort -from .dashboard.team_settings import get_team_default_model_pair +from .dashboard.team_settings import get_team_default_model_pair, get_team_default_repo from .integrations.langsmith import _configure_github_proxy from .middleware import ( ModelFallbackMiddleware, @@ -100,6 +100,24 @@ from .utils.sandbox_state import ( ) +async def _resolve_prompt_default_repo(configurable: dict[str, Any]) -> dict[str, str] | None: + repo_config = configurable.get("repo") + if isinstance(repo_config, dict): + owner = repo_config.get("owner") + name = repo_config.get("name") + if isinstance(owner, str) and isinstance(name, str): + return {"owner": owner, "name": name} + + if configurable.get("repo_explicitly_none") is True: + return None + + try: + return await get_team_default_repo() + except Exception: + logger.debug("Failed to load team default repo for prompt", exc_info=True) + return None + + async def _start_langsmith_sandbox_if_needed(sandbox_backend: SandboxBackendProtocol) -> None: """Start a LangSmith sandbox before operations that require it to be running.""" if os.getenv("SANDBOX_TYPE", "langsmith") != "langsmith": @@ -541,6 +559,8 @@ async def get_agent(config: RunnableConfig) -> Pregel: except Exception: logger.debug("Failed to record agent usage for thread %s", thread_id, exc_info=True) + prompt_default_repo = await _resolve_prompt_default_repo(configurable) + logger.info("Returning agent with sandbox for thread %s", thread_id) main_model = make_model(model_id, **model_kwargs) subagent_model = make_model(subagent_model_id, **subagent_model_kwargs) @@ -552,6 +572,7 @@ async def get_agent(config: RunnableConfig) -> Pregel: linear_issue_number=linear_issue_number, triggering_user_identity=triggering_user_identity, create_prs=always_create_prs, + default_repo=prompt_default_repo, ), tools=[ http_request, diff --git a/agent/webapp.py b/agent/webapp.py index 3bcf721a..ce8d77d4 100644 --- a/agent/webapp.py +++ b/agent/webapp.py @@ -27,7 +27,7 @@ from .dashboard.agent_overrides import ( from .dashboard.enabled_repos import is_review_repo_enabled from .dashboard.oauth import build_settings_url from .dashboard.profiles import get_profile, get_valid_access_token, has_access_token_record -from .dashboard.team_settings import get_team_settings +from .dashboard.team_settings import get_team_default_repo, get_team_settings from .dashboard.user_mappings import ( email_for_login, login_for_email, @@ -137,7 +137,7 @@ SLACK_SIGNING_SECRET = os.environ.get("SLACK_SIGNING_SECRET", "") SLACK_BOT_USER_ID = os.environ.get("SLACK_BOT_USER_ID", "") SLACK_BOT_USERNAME = os.environ.get("SLACK_BOT_USERNAME", "") DEFAULT_REPO_OWNER = os.environ.get("DEFAULT_REPO_OWNER", "langchain-ai") -DEFAULT_REPO_NAME = os.environ.get("DEFAULT_REPO_NAME", "langchainplus") +DEFAULT_REPO_NAME = os.environ.get("DEFAULT_REPO_NAME", "") SLACK_REPO_OWNER = os.environ.get("SLACK_REPO_OWNER", "") or DEFAULT_REPO_OWNER SLACK_REPO_NAME = os.environ.get("SLACK_REPO_NAME", "") or DEFAULT_REPO_NAME @@ -183,7 +183,7 @@ def get_repo_config_from_team_mapping( team_identifier: str, project_name: str = "" ) -> dict[str, str]: """Look up repository configuration from LINEAR_TEAM_TO_REPO mapping.""" - fallback = {"owner": DEFAULT_REPO_OWNER, "name": DEFAULT_REPO_NAME} + fallback = {"owner": DEFAULT_REPO_OWNER, "name": DEFAULT_REPO_NAME} if DEFAULT_REPO_NAME else {} if not team_identifier or team_identifier not in LINEAR_TEAM_TO_REPO: return fallback @@ -608,8 +608,14 @@ async def get_slack_repo_config( logger.exception("Failed to apply dashboard default_repo for Slack user") if not repo_config: + repo_config = await get_team_default_repo() + + if not repo_config and default_owner and default_name: repo_config = {"owner": default_owner, "name": default_name} + if not repo_config: + raise HTTPException(400, "no default repository configured") + return repo_config @@ -1305,6 +1311,12 @@ async def linear_webhook( # noqa: PLR0911, PLR0912, PLR0915 }, ) + if not repo_config: + repo_config = await get_team_default_repo() + + if not repo_config: + return {"status": "ignored", "reason": "No default repository configured"} + if not _is_repo_allowed(repo_config): logger.warning( "Rejecting Linear webhook: repo '%s/%s' not in allowlist", diff --git a/default_prompt.md b/default_prompt.md index c41fa5b8..c9dd8b22 100644 --- a/default_prompt.md +++ b/default_prompt.md @@ -1,3 +1,3 @@ # Default Prompt -When a repository is not explicitly mentioned, use `langchain-ai/langchainplus`. Always assume the organization is `langchain-ai`. +When a repository is not explicitly mentioned, use the repository provided in the run metadata or dashboard settings. Do not assume a hardcoded repository name. diff --git a/tests/test_dashboard_repo_optional.py b/tests/test_dashboard_repo_optional.py index 711140e1..025b3a3b 100644 --- a/tests/test_dashboard_repo_optional.py +++ b/tests/test_dashboard_repo_optional.py @@ -1,5 +1,10 @@ from __future__ import annotations +import asyncio +from typing import Any + +import pytest + from agent.dashboard import thread_api @@ -8,7 +13,6 @@ def test_resolve_repo_config_parses_request_repo() -> None: def test_resolve_repo_config_returns_empty_when_no_repo_given() -> None: - # None / blank / malformed all mean an intentionally repo-less run — never an error. assert thread_api._resolve_repo_config(None) == {} assert thread_api._resolve_repo_config("") == {} assert thread_api._resolve_repo_config("not-a-repo") == {} @@ -36,3 +40,116 @@ def test_thread_summary_keeps_repo_when_present() -> None: ) assert summary["repo"] == "repo" assert summary["repoFullName"] == "octo/repo" + + +class _FakeThreadsClient: + async def create( + self, *, thread_id: str, metadata: dict[str, Any], if_exists: str + ) -> dict[str, Any]: + return {"thread_id": thread_id, "metadata": metadata, "if_exists": if_exists} + + async def update(self, *, thread_id: str, metadata: dict[str, Any]) -> dict[str, Any]: + return {"thread_id": thread_id, "metadata": metadata} + + async def get(self, thread_id: str) -> dict[str, Any]: + return {"thread_id": thread_id, "metadata": {}} + + +class _FakeRunsClient: + def __init__(self) -> None: + self.configurable: dict[str, Any] | None = None + + async def create( + self, + thread_id: str, + assistant_id: str, + *, + input: dict[str, Any], + config: dict[str, Any], + if_not_exists: str = "reject", + stream_mode: list[str] | None = None, + stream_resumable: bool = False, + ) -> dict[str, str]: + self.configurable = config["configurable"] + return {"run_id": "run-id"} + + +class _FakeLangGraphClient: + def __init__(self) -> None: + self.threads = _FakeThreadsClient() + self.runs = _FakeRunsClient() + + +@pytest.fixture +def dashboard_run_client(monkeypatch: pytest.MonkeyPatch) -> _FakeLangGraphClient: + client = _FakeLangGraphClient() + + async def fake_get_profile(login: str) -> dict[str, Any]: + return {} + + async def fake_ensure_token(login: str) -> None: + return None + + async def fake_resolve_email(login: str, profile: dict[str, Any]) -> str: + return "octo@example.com" + + monkeypatch.setattr(thread_api, "langgraph_client", lambda: client) + monkeypatch.setattr(thread_api, "get_profile", fake_get_profile) + monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", fake_ensure_token) + monkeypatch.setattr(thread_api, "_resolve_run_email", fake_resolve_email) + return client + + +def test_start_agent_run_omits_repo_less_marker_when_repo_unset( + dashboard_run_client: _FakeLangGraphClient, +) -> None: + asyncio.run( + thread_api._start_agent_run( + "thread-id", + login="octo", + repo_config={}, + prompt="do work", + ) + ) + + configurable = dashboard_run_client.runs.configurable + assert configurable is not None + assert "repo_explicitly_none" not in configurable + assert "repo" not in configurable + + +def test_start_agent_run_marks_repo_less_config_when_explicit( + dashboard_run_client: _FakeLangGraphClient, +) -> None: + asyncio.run( + thread_api._start_agent_run( + "thread-id", + login="octo", + repo_config={}, + repo_explicitly_none=True, + prompt="do work", + ) + ) + + configurable = dashboard_run_client.runs.configurable + assert configurable is not None + assert configurable["repo_explicitly_none"] is True + assert "repo" not in configurable + + +def test_start_agent_run_omits_repo_less_marker_when_repo_configured( + dashboard_run_client: _FakeLangGraphClient, +) -> None: + asyncio.run( + thread_api._start_agent_run( + "thread-id", + login="octo", + repo_config={"owner": "octo", "name": "repo"}, + prompt="do work", + ) + ) + + configurable = dashboard_run_client.runs.configurable + assert configurable is not None + assert configurable["repo"] == {"owner": "octo", "name": "repo"} + assert "repo_explicitly_none" not in configurable diff --git a/tests/test_dashboard_web_handoff.py b/tests/test_dashboard_web_handoff.py index 2bba563d..4fd13b4a 100644 --- a/tests/test_dashboard_web_handoff.py +++ b/tests/test_dashboard_web_handoff.py @@ -120,3 +120,58 @@ async def test_dashboard_followup_on_busy_thread_queues_dashboard_handoff( assert client.threads.updates[0]["source"] == "dashboard" assert queued_messages == [{"text": "continue in web", "source": "dashboard"}] + + +@pytest.mark.asyncio +async def test_dashboard_followup_preserves_explicit_repo_less_thread( + monkeypatch: pytest.MonkeyPatch, +) -> None: + metadata = { + "source": "dashboard", + "github_login": "octocat", + "repo_explicitly_none": True, + } + client = _FakeClient(metadata) + + monkeypatch.setattr(thread_api, "langgraph_client", lambda: client) + monkeypatch.setattr(thread_api, "is_thread_active", _inactive_thread) + monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", _noop_token_check) + monkeypatch.setattr(thread_api, "get_profile", _empty_profile) + monkeypatch.setattr(thread_api, "_resolve_run_email", _run_email) + + await thread_api.send_dashboard_message( + "thread-1", + "octocat", + thread_api.ThreadMessageBody(content="continue in web"), + ) + + run_config = client.runs.created[0]["kwargs"]["config"]["configurable"] + assert run_config["repo_explicitly_none"] is True + assert "repo" not in run_config + + +@pytest.mark.asyncio +async def test_dashboard_followup_without_repo_metadata_allows_team_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + metadata = { + "source": "dashboard", + "github_login": "octocat", + } + client = _FakeClient(metadata) + + monkeypatch.setattr(thread_api, "langgraph_client", lambda: client) + monkeypatch.setattr(thread_api, "is_thread_active", _inactive_thread) + monkeypatch.setattr(thread_api, "_ensure_dashboard_github_token", _noop_token_check) + monkeypatch.setattr(thread_api, "get_profile", _empty_profile) + monkeypatch.setattr(thread_api, "_resolve_run_email", _run_email) + + await thread_api.send_dashboard_message( + "thread-1", + "octocat", + thread_api.ThreadMessageBody(content="continue in web"), + ) + + run_config = client.runs.created[0]["kwargs"]["config"]["configurable"] + assert "repo_explicitly_none" not in run_config + assert "repo" not in run_config diff --git a/tests/test_prompt_default_repo.py b/tests/test_prompt_default_repo.py new file mode 100644 index 00000000..bf52f459 --- /dev/null +++ b/tests/test_prompt_default_repo.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +import asyncio + +import pytest + +from agent import server + + +def test_resolve_prompt_default_repo_uses_explicit_repo( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def fake_get_team_default_repo() -> dict[str, str] | None: + raise AssertionError("team default should not be loaded") + + monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo) + + repo = asyncio.run( + server._resolve_prompt_default_repo({"repo": {"owner": "octo", "name": "repo"}}) + ) + + assert repo == {"owner": "octo", "name": "repo"} + + +def test_resolve_prompt_default_repo_skips_team_default_for_repo_less_run( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def fake_get_team_default_repo() -> dict[str, str] | None: + raise AssertionError("team default should not be loaded") + + monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo) + + repo = asyncio.run(server._resolve_prompt_default_repo({"repo_explicitly_none": True})) + + assert repo is None + + +def test_resolve_prompt_default_repo_falls_back_to_team_default( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def fake_get_team_default_repo() -> dict[str, str] | None: + return {"owner": "team", "name": "repo"} + + monkeypatch.setattr(server, "get_team_default_repo", fake_get_team_default_repo) + + repo = asyncio.run(server._resolve_prompt_default_repo({})) + + assert repo == {"owner": "team", "name": "repo"} diff --git a/tests/test_slack_context.py b/tests/test_slack_context.py index 99c61e29..2e475c71 100644 --- a/tests/test_slack_context.py +++ b/tests/test_slack_context.py @@ -395,6 +395,24 @@ def test_get_slack_repo_config_applies_profile_default_repo( assert repo == {"owner": "profile-owner", "name": "profile-repo"} +def test_get_slack_repo_config_applies_team_default_repo( + monkeypatch: pytest.MonkeyPatch, +) -> None: + threads_client = _FakeThreadsClient(thread={"metadata": {}}) + + async def fake_get_team_default_repo() -> dict[str, str] | None: + return {"owner": "team-owner", "name": "team-repo"} + + monkeypatch.setattr(webapp, "get_client", lambda url: _FakeClient(threads_client)) + monkeypatch.setattr(webapp, "get_team_default_repo", fake_get_team_default_repo) + monkeypatch.setattr(webapp, "SLACK_REPO_NAME", "") + monkeypatch.setattr(webapp, "DEFAULT_REPO_NAME", "") + + repo = asyncio.run(webapp.get_slack_repo_config("C123", "1.234")) + + assert repo == {"owner": "team-owner", "name": "team-repo"} + + def _setup_slack_mention_fakes( monkeypatch: pytest.MonkeyPatch, captured: dict[str, object] ) -> None: diff --git a/ui/src/components/agents/AgentsHome.tsx b/ui/src/components/agents/AgentsHome.tsx index 635f209f..7067d66c 100644 --- a/ui/src/components/agents/AgentsHome.tsx +++ b/ui/src/components/agents/AgentsHome.tsx @@ -36,6 +36,7 @@ export function AgentsHome() { createThread.mutate({ prompt, repo, + repo_explicitly_none: repoOverride === null, model_id: activeSelection?.modelId ?? null, effort: activeSelection?.effort ?? null, }) diff --git a/ui/src/lib/agents/api.ts b/ui/src/lib/agents/api.ts index b385035d..343a8f73 100644 --- a/ui/src/lib/agents/api.ts +++ b/ui/src/lib/agents/api.ts @@ -5,6 +5,7 @@ export type { AgentSchedule, AgentThread, Message } export interface ThreadCreateRequest { prompt: string repo?: string | null + repo_explicitly_none?: boolean model_id?: string | null effort?: string | null } diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 01aabf29..a06955b5 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -113,6 +113,7 @@ export interface TeamSettings { default_agent_reasoning_effort?: string | null; default_agent_subagent_model?: string | null; default_agent_subagent_reasoning_effort?: string | null; + default_repo?: string | null; default_reviewer_model?: string | null; default_reviewer_reasoning_effort?: string | null; default_reviewer_subagent_model?: string | null; diff --git a/ui/src/routes/admin.tsx b/ui/src/routes/admin.tsx index 7a2ef557..79ca1e45 100644 --- a/ui/src/routes/admin.tsx +++ b/ui/src/routes/admin.tsx @@ -5,6 +5,7 @@ import { useEffect, useState } from "react"; import type { ModelOption, TeamSettings, UserMapping } from "@/lib/api"; import { AppShell, SettingsRow, SettingsSection } from "@/components/AppShell"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Select, SelectContent, @@ -156,6 +157,11 @@ function GlobalDefaultsSection({ models }: { models: Array }) { queryFn: api.getTeamSettings, }); const [error, setError] = useState(null); + const [defaultRepoDraft, setDefaultRepoDraft] = useState(""); + + useEffect(() => { + setDefaultRepoDraft(settings.data?.default_repo ?? ""); + }, [settings.data?.default_repo]); const save = useMutation({ mutationFn: (body: TeamSettings) => api.saveTeamSettings(body), @@ -204,6 +210,26 @@ function GlobalDefaultsSection({ models }: { models: Array }) { } disabled={!settings.data || save.isPending} /> + setDefaultRepoDraft(e.target.value)} + onBlur={() => + settings.data && + save.mutate({ + ...settings.data, + default_repo: defaultRepoDraft.trim() || null, + }) + } + disabled={!settings.data || save.isPending} + /> + } + />