mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): highlight just-installed provider on /auth reopen (#4311)
Fixed the `/auth` manager so that after installing a provider it reopens with the just-installed provider highlighted instead of resetting to the top of the list. Made by [Open SWE](https://openswe.vercel.app/agents/d1b42eb9-0efa-0224-dcc2-c71ad53093cd) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -10162,12 +10162,17 @@ class DeepAgentsApp(App):
|
||||
)
|
||||
self.push_screen(screen, handle_result)
|
||||
|
||||
async def _show_auth_manager(self) -> None:
|
||||
async def _show_auth_manager(self, *, initial_provider: str | None = None) -> None:
|
||||
"""Show the `/auth` credential manager modal.
|
||||
|
||||
State changes persist via `auth_store`; the manager refreshes its
|
||||
own option labels after each save/delete, so this caller only needs
|
||||
to refocus the chat input on close.
|
||||
|
||||
Args:
|
||||
initial_provider: Provider to start highlighted — set when
|
||||
reopening after an install-on-select so the cursor lands on
|
||||
the just-installed provider instead of the top of the list.
|
||||
"""
|
||||
from deepagents_code.widgets.auth import AuthManagerScreen
|
||||
|
||||
@@ -10182,13 +10187,17 @@ class DeepAgentsApp(App):
|
||||
from functools import partial
|
||||
|
||||
self.call_later(
|
||||
partial(self._install_provider_then_reopen_auth, extra),
|
||||
partial(
|
||||
self._install_provider_then_reopen_auth,
|
||||
extra,
|
||||
provider=screen.pending_install_provider,
|
||||
),
|
||||
)
|
||||
return
|
||||
task = asyncio.create_task(self._resume_server_after_auth_change())
|
||||
task.add_done_callback(_log_task_exception)
|
||||
|
||||
screen = AuthManagerScreen()
|
||||
screen = AuthManagerScreen(initial_provider=initial_provider)
|
||||
self.push_screen(screen, handle_result)
|
||||
|
||||
def on_auth_manager_screen_credential_saved(self, event: Message) -> None:
|
||||
@@ -10258,14 +10267,18 @@ class DeepAgentsApp(App):
|
||||
await self._retry_startup_with_model(model_spec, extra_kwargs=extra_kwargs)
|
||||
return True
|
||||
|
||||
async def _install_provider_then_reopen_auth(self, extra: str) -> None:
|
||||
async def _install_provider_then_reopen_auth(
|
||||
self, extra: str, *, provider: str | None = None
|
||||
) -> None:
|
||||
"""Install a provider's extra from `/auth`, then reopen the manager.
|
||||
|
||||
Args:
|
||||
extra: The extra that installs the selected provider's integration.
|
||||
provider: The provider being installed, highlighted in the
|
||||
reopened manager so the cursor lands on it ready for a key.
|
||||
"""
|
||||
if await self._install_extra(extra, auto_restart=True):
|
||||
await self._show_auth_manager()
|
||||
await self._show_auth_manager(initial_provider=provider)
|
||||
return
|
||||
# `_install_extra` returns `False` both when the install genuinely
|
||||
# failed (it already surfaced the reason) and when the package landed
|
||||
@@ -10277,7 +10290,7 @@ class DeepAgentsApp(App):
|
||||
from deepagents_code.model_config import clear_caches
|
||||
|
||||
clear_caches()
|
||||
await self._show_auth_manager()
|
||||
await self._show_auth_manager(initial_provider=provider)
|
||||
return
|
||||
if ready is None:
|
||||
# Introspection couldn't confirm the state (rare). Don't dead-end
|
||||
@@ -10288,6 +10301,12 @@ class DeepAgentsApp(App):
|
||||
"Reopen `/auth` to add a key once it has.",
|
||||
),
|
||||
)
|
||||
return
|
||||
# `ready is False`: the extra genuinely didn't install. `_install_extra`
|
||||
# has already surfaced the reason to the user, so stay in chat rather
|
||||
# than reopen — but log it so an "install button did nothing" report is
|
||||
# debuggable without relying on that sibling method's invariant.
|
||||
logger.debug("Provider extra %r not importable after install attempt", extra)
|
||||
|
||||
def _switch_agent(self, agent_name: str) -> None:
|
||||
"""Switch to a different agent and hot-restart the backing server.
|
||||
|
||||
@@ -31,7 +31,7 @@ from textual.message import Message
|
||||
from textual.screen import ModalScreen
|
||||
from textual.style import Style as TStyle
|
||||
from textual.widgets import Input, OptionList, Static
|
||||
from textual.widgets.option_list import Option
|
||||
from textual.widgets.option_list import Option, OptionDoesNotExist
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from textual.app import ComposeResult
|
||||
@@ -1148,16 +1148,26 @@ class AuthManagerScreen(ModalScreen[None]):
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the manager with an empty install-on-select registry."""
|
||||
def __init__(self, *, initial_provider: str | None = None) -> None:
|
||||
"""Initialize the manager with an empty install-on-select registry.
|
||||
|
||||
Args:
|
||||
initial_provider: Provider whose row should start highlighted —
|
||||
set when reopening after an install-on-select so the cursor
|
||||
lands on the just-installed provider ready for a key, rather
|
||||
than resetting to the top of the list.
|
||||
"""
|
||||
super().__init__()
|
||||
# Uninstalled known providers mapped to the extra that installs them,
|
||||
# populated each time the option list is built. Selecting one routes
|
||||
# to the install confirmation instead of the key prompt.
|
||||
self._install_extras: dict[str, str] = {}
|
||||
# Set when the user confirms installing a provider's extra; the app
|
||||
# reads this off the screen after dismissal to install then reopen.
|
||||
# reads these off the screen after dismissal to install then reopen
|
||||
# the manager with the just-installed provider highlighted.
|
||||
self.pending_install_extra: str | None = None
|
||||
self.pending_install_provider: str | None = None
|
||||
self._initial_provider = initial_provider
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
"""Compose the manager.
|
||||
@@ -1213,11 +1223,29 @@ class AuthManagerScreen(ModalScreen[None]):
|
||||
)
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Apply ASCII border when needed."""
|
||||
"""Apply ASCII border and highlight the initial provider when set."""
|
||||
if is_ascii_mode():
|
||||
container = self.query_one(Vertical)
|
||||
colors = theme.get_theme_colors(self)
|
||||
container.styles.border = ("ascii", colors.success)
|
||||
self._highlight_initial_provider()
|
||||
|
||||
def _highlight_initial_provider(self) -> None:
|
||||
"""Move the cursor to `initial_provider`'s row if it is listed.
|
||||
|
||||
Used when the manager reopens after an install-on-select so the cursor
|
||||
lands on the just-installed provider (ready for a key) instead of
|
||||
resetting to the top of the list.
|
||||
"""
|
||||
if self._initial_provider is None:
|
||||
return
|
||||
option_list = self.query_one("#auth-manager-options", OptionList)
|
||||
try:
|
||||
index = option_list.get_option_index(self._initial_provider)
|
||||
except OptionDoesNotExist:
|
||||
return
|
||||
option_list.highlighted = index
|
||||
option_list.scroll_to_highlight()
|
||||
|
||||
def on_click(self, event: Click) -> None: # noqa: PLR6301 - Textual handler
|
||||
"""Open style-embedded hyperlinks (the title `Docs` link)."""
|
||||
@@ -1285,11 +1313,13 @@ class AuthManagerScreen(ModalScreen[None]):
|
||||
def _on_confirm(proceed: bool | None) -> None:
|
||||
if proceed:
|
||||
self.pending_install_extra = extra
|
||||
self.pending_install_provider = provider
|
||||
self.dismiss(None)
|
||||
else:
|
||||
# Declined or dismissed: clear any pending extra so a reused
|
||||
# Declined or dismissed: clear any pending request so a reused
|
||||
# screen never carries a stale install request, and stay put.
|
||||
self.pending_install_extra = None
|
||||
self.pending_install_provider = None
|
||||
|
||||
self.app.push_screen(
|
||||
InstallProviderConfirmScreen(provider, extra),
|
||||
|
||||
@@ -7826,10 +7826,25 @@ class TestDefaultAgentNameDrift:
|
||||
class TestInstallExtraAuthContinuation:
|
||||
"""Test `/auth` reopening after installing provider extras."""
|
||||
|
||||
async def test_reopens_auth_with_provider_highlighted_after_install(self) -> None:
|
||||
"""A successful install reopens the manager on the just-installed provider."""
|
||||
app = DeepAgentsApp()
|
||||
app._install_extra = AsyncMock(return_value=True) # ty: ignore
|
||||
app._show_auth_manager = AsyncMock() # ty: ignore
|
||||
|
||||
await app._install_provider_then_reopen_auth("baseten", provider="baseten")
|
||||
|
||||
app._install_extra.assert_awaited_once_with("baseten", auto_restart=True) # ty: ignore
|
||||
app._show_auth_manager.assert_awaited_once_with(initial_provider="baseten") # ty: ignore
|
||||
|
||||
async def test_reopens_auth_after_installed_extra_even_when_restart_fails(
|
||||
self,
|
||||
) -> None:
|
||||
"""`/auth` only needs the install to land before reopening the manager."""
|
||||
"""`/auth` only needs the install to land before reopening the manager.
|
||||
|
||||
Even on the restart-failed path the just-installed provider is threaded
|
||||
through so the reopened manager lands the cursor on its row.
|
||||
"""
|
||||
app = DeepAgentsApp()
|
||||
app._install_extra = AsyncMock(return_value=False) # ty: ignore
|
||||
app._show_auth_manager = AsyncMock() # ty: ignore
|
||||
@@ -7838,23 +7853,29 @@ class TestInstallExtraAuthContinuation:
|
||||
patch("deepagents_code.app._extra_is_ready", return_value=True),
|
||||
patch("deepagents_code.model_config.clear_caches") as clear_caches,
|
||||
):
|
||||
await app._install_provider_then_reopen_auth("baseten")
|
||||
await app._install_provider_then_reopen_auth("baseten", provider="baseten")
|
||||
|
||||
app._install_extra.assert_awaited_once_with("baseten", auto_restart=True) # ty: ignore
|
||||
clear_caches.assert_called_once_with()
|
||||
app._show_auth_manager.assert_awaited_once() # ty: ignore
|
||||
app._show_auth_manager.assert_awaited_once_with(initial_provider="baseten") # ty: ignore
|
||||
|
||||
async def test_does_not_reopen_auth_when_install_failed(self) -> None:
|
||||
"""A failed install still leaves the user in chat with the surfaced error."""
|
||||
async def test_does_not_reopen_auth_when_install_failed(
|
||||
self, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""A failed install leaves the user in chat and logs the dead-end at DEBUG."""
|
||||
app = DeepAgentsApp()
|
||||
app._install_extra = AsyncMock(return_value=False) # ty: ignore
|
||||
app._show_auth_manager = AsyncMock() # ty: ignore
|
||||
|
||||
with patch("deepagents_code.app._extra_is_ready", return_value=False):
|
||||
await app._install_provider_then_reopen_auth("baseten")
|
||||
with (
|
||||
patch("deepagents_code.app._extra_is_ready", return_value=False),
|
||||
caplog.at_level(logging.DEBUG, logger="deepagents_code.app"),
|
||||
):
|
||||
await app._install_provider_then_reopen_auth("baseten", provider="baseten")
|
||||
|
||||
app._install_extra.assert_awaited_once_with("baseten", auto_restart=True) # ty: ignore
|
||||
app._show_auth_manager.assert_not_awaited() # ty: ignore
|
||||
assert any("baseten" in record.message for record in caplog.records)
|
||||
|
||||
async def test_surfaces_hint_when_install_state_unverifiable(self) -> None:
|
||||
"""An unknown post-install state points the user back to `/auth`.
|
||||
@@ -7869,7 +7890,7 @@ class TestInstallExtraAuthContinuation:
|
||||
app._mount_message = AsyncMock() # ty: ignore
|
||||
|
||||
with patch("deepagents_code.app._extra_is_ready", return_value=None):
|
||||
await app._install_provider_then_reopen_auth("baseten")
|
||||
await app._install_provider_then_reopen_auth("baseten", provider="baseten")
|
||||
|
||||
app._show_auth_manager.assert_not_awaited() # ty: ignore
|
||||
app._mount_message.assert_awaited_once() # ty: ignore
|
||||
|
||||
@@ -93,9 +93,9 @@ class _AuthHostApp(App[None]):
|
||||
handle,
|
||||
)
|
||||
|
||||
def show_manager(self) -> None:
|
||||
def show_manager(self, *, initial_provider: str | None = None) -> None:
|
||||
"""Push the manager screen."""
|
||||
self.push_screen(AuthManagerScreen())
|
||||
self.push_screen(AuthManagerScreen(initial_provider=initial_provider))
|
||||
|
||||
def on_auth_manager_screen_credential_saved(
|
||||
self, _event: AuthManagerScreen.CredentialSaved
|
||||
@@ -1680,6 +1680,53 @@ api_key_env = "MY_GATEWAY_API_KEY"
|
||||
await pilot.press("enter")
|
||||
await pilot.pause()
|
||||
assert screen.pending_install_extra == "groq"
|
||||
assert screen.pending_install_provider == "groq"
|
||||
|
||||
async def test_reopening_manager_highlights_initial_provider(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Reopening after an install highlights the just-installed provider.
|
||||
|
||||
Simulates the post-install reopen: `groq` is now installed and listed,
|
||||
and passing it as `initial_provider` lands the cursor on its row rather
|
||||
than resetting to index 0.
|
||||
"""
|
||||
monkeypatch.setattr(
|
||||
"deepagents_code.widgets.auth.get_available_models",
|
||||
lambda: {"openai": ["gpt-5.4"], "groq": ["llama-3"]},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"deepagents_code.config_manifest.is_provider_package_installed",
|
||||
lambda provider: provider in {"openai", "groq"},
|
||||
)
|
||||
app = _AuthHostApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.show_manager(initial_provider="groq")
|
||||
await pilot.pause()
|
||||
screen = cast("AuthManagerScreen", app.screen)
|
||||
options = screen.query_one("#auth-manager-options", OptionList)
|
||||
assert options.highlighted is not None
|
||||
assert options.get_option_at_index(options.highlighted).id == "groq"
|
||||
|
||||
async def test_reopening_manager_ignores_unknown_initial_provider(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""An initial provider absent from the list leaves the cursor at the top."""
|
||||
monkeypatch.setattr(
|
||||
"deepagents_code.widgets.auth.get_available_models",
|
||||
lambda: {"openai": ["gpt-5.4"]},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"deepagents_code.config_manifest.is_provider_package_installed",
|
||||
lambda provider: provider == "openai",
|
||||
)
|
||||
app = _AuthHostApp()
|
||||
async with app.run_test() as pilot:
|
||||
app.show_manager(initial_provider="not-a-real-provider")
|
||||
await pilot.pause()
|
||||
screen = cast("AuthManagerScreen", app.screen)
|
||||
options = screen.query_one("#auth-manager-options", OptionList)
|
||||
assert options.highlighted == 0
|
||||
|
||||
async def test_cancelling_install_leaves_manager_without_pending_extra(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
@@ -1703,6 +1750,7 @@ api_key_env = "MY_GATEWAY_API_KEY"
|
||||
await pilot.press("escape")
|
||||
await pilot.pause()
|
||||
assert screen.pending_install_extra is None
|
||||
assert screen.pending_install_provider is None
|
||||
assert isinstance(app.screen, AuthManagerScreen)
|
||||
|
||||
async def test_disabled_known_provider_not_offered_for_install(
|
||||
|
||||
Reference in New Issue
Block a user